diff options
author | Andrew Newdigate <andrew@gitlab.com> | 2017-09-07 10:58:28 +0100 |
---|---|---|
committer | Andrew Newdigate <andrew@gitlab.com> | 2017-09-07 10:58:28 +0100 |
commit | aee3e2bc908accad77cd5b1f4594e3da3844a34d (patch) | |
tree | 42151c8228fa52fa42ff30d9dd21ac9aa3be3603 | |
parent | 632d6cd0306ee341271188616337d9831f097ccf (diff) | |
parent | 8f7638798dc91373c4b58d697e11f01f99a4ca6d (diff) | |
download | gitlab-ce-gitaly-feature-toggles-development-opt-out.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into gitaly-feature-toggles-development-opt-outgitaly-feature-toggles-development-opt-out
512 files changed, 11660 insertions, 4080 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aa7659d1b41..7b42e661dff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,6 +43,7 @@ stages: # Predefined scopes .dedicated-runner: &dedicated-runner + retry: 1 tags: - gitlab-org @@ -208,11 +209,10 @@ update-tests-metadata: - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - - rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json + - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json flaky-examples-check: <<: *dedicated-runner - <<: *except-docs image: ruby:2.3-alpine services: [] before_script: [] @@ -227,6 +227,7 @@ flaky-examples-check: - branches except: - master + - /(^docs[\/-].*|.*-docs$)/ artifacts: expire_in: 30d paths: @@ -246,7 +247,6 @@ setup-test-env: script: - node --version - yarn install --frozen-lockfile --cache-folder .yarn-cache - - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - scripts/gitaly-test-build # Do not use 'bundle exec' here @@ -411,7 +411,6 @@ db:migrate:reset-mysql: .migration-paths: &migration-paths <<: *dedicated-runner - <<: *only-canonical-masters <<: *pull-cache stage: test variables: @@ -495,7 +494,6 @@ gitlab:assets:compile: NO_COMPRESSION: "true" script: - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - - bundle exec rake gettext:po_to_json - bundle exec rake gitlab:assets:compile artifacts: name: webpack-report diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 93d4c1ef06f..ca75280b09b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.36.0 +0.38.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 11d9efa3d5a..b3d91f9cfc0 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.8.0 +5.9.0 @@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0' gem 'hipchat', '~> 1.5.0' # JIRA integration -gem 'jira-ruby', '~> 1.1.2' +gem 'jira-ruby', '~> 1.4' # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' @@ -324,7 +324,7 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.4' - gem 'capybara', '~> 2.6.2' + gem 'capybara', '~> 2.15.0' gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' @@ -397,7 +397,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index cba30e856ed..5e3d12544fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,9 +100,9 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (9.0.6) - capybara (2.6.2) + capybara (2.15.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) @@ -275,7 +275,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.31.0) + gitaly-proto (0.33.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -404,8 +404,9 @@ GEM cause json ipaddress (0.8.3) - jira-ruby (1.1.2) + jira-ruby (1.4.1) activesupport + multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) @@ -478,6 +479,7 @@ GEM method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) + mini_mime (0.1.4) mini_portile2 (2.2.0) minitest (5.7.0) mmap2 (2.2.7) @@ -947,7 +949,7 @@ GEM expression_parser rinku xml-simple (1.1.5) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS @@ -978,7 +980,7 @@ DEPENDENCIES browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) - capybara (~> 2.6.2) + capybara (~> 2.15.0) capybara-screenshot (~> 1.0.0) carrierwave (~> 1.1) charlock_holmes (~> 0.7.5) @@ -1020,7 +1022,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.31.0) + gitaly-proto (~> 0.33.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) @@ -1042,7 +1044,7 @@ DEPENDENCIES html2text httparty (~> 0.13.3) influxdb (~> 0.2) - jira-ruby (~> 1.1.2) + jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) json-schema (~> 2.6.2) diff --git a/PROCESS.md b/PROCESS.md index 538e4389e00..ed4e84dd0b6 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -199,26 +199,8 @@ available in the package repositories. ## Release retrospective and kickoff -### Retrospective - -After each release, we have a retrospective call where we discuss what went well, -what went wrong, and what we can improve for the next release. The -[retrospective notes] are public and you are invited to comment on them. -If you're interested, you can even join the -[retrospective call][retro-kickoff-call], on the first working day after the -22nd at 6pm CET / 9am PST. - -### Kickoff - -Before working on the next release, we have a -kickoff call to explain what we expect to ship in the next release. The -[kickoff notes] are public and you are invited to comment on them. -If you're interested, you can even join the [kickoff call][retro-kickoff-call], -on the first working day after the 7th at 6pm CET / 9am PST.. - -[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing -[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing -[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206 +- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective) +- [Kickoff](https://about.gitlab.com/handbook/engineering/workflow/#kickoff) ## Copy & paste responses diff --git a/README.md b/README.md index 9309922ae39..9ead6d51c5d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) +[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 78cb3def879..8acddd6194c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,7 +5,7 @@ const Api = { groupPath: '/api/:version/groups/:id.json', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', - projectsPath: '/api/:version/projects.json?simple=true', + projectsPath: '/api/:version/projects.json', labelsPath: '/:namespace_path/:project_path/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', @@ -58,6 +58,7 @@ const Api = { const defaults = { search: query, per_page: 20, + simple: true, }; if (gon.current_user_id) { diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js new file mode 100644 index 00000000000..10fbcfe96cf --- /dev/null +++ b/app/assets/javascripts/breadcrumb.js @@ -0,0 +1,28 @@ +export const addTooltipToEl = (el) => { + const textEl = el.querySelector('.js-breadcrumb-item-text'); + + if (textEl && textEl.scrollWidth > textEl.offsetWidth) { + el.setAttribute('title', el.textContent); + el.setAttribute('data-container', 'body'); + el.classList.add('has-tooltip'); + } +}; + +export default () => { + const breadcrumbs = document.querySelector('.js-breadcrumbs-list'); + + if (breadcrumbs) { + const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown')) + .map(el => el.querySelector('a')) + .filter(el => el); + const $expander = $('.js-breadcrumbs-collapsed-expander'); + + topLevelLinks.forEach(el => addTooltipToEl(el)); + + $expander.closest('.dropdown') + .on('show.bs.dropdown hide.bs.dropdown', (e) => { + $('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open') + .tooltip('hide'); + }); + } +}; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 6db8b3afbef..768453b28f1 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -2,3 +2,4 @@ import 'underscore'; import './polyfills'; import './jquery'; import './bootstrap'; +import './vue'; diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/commons/vue.js index eb2a6071fda..8b62d78c043 100644 --- a/app/assets/javascripts/vue_shared/common_vue.js +++ b/app/assets/javascripts/commons/vue.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import './vue_resource_interceptor'; if (process.env.NODE_ENV !== 'production') { Vue.config.productionTip = false; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3dec4de06ec..6db0b18ae5a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -41,7 +41,6 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import DeleteModal from './branches/branches_delete_modal'; import Group from './group'; -import GroupName from './group_name'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import setupProjectEdit from './project_edit'; @@ -489,6 +488,8 @@ import initChangesDropdown from './init_changes_dropdown'; initSettingsPanels(); break; case 'projects:settings:ci_cd:show': + // Initialize expandable settings panels + initSettingsPanels(); case 'groups:settings:ci_cd:show': new gl.ProjectVariables(); break; @@ -554,9 +555,6 @@ import initChangesDropdown from './init_changes_dropdown'; case 'root': new UserCallout(); break; - case 'groups': - new GroupName(); - break; case 'profiles': new NotificationsForm(); new NotificationsDropdown(); @@ -564,7 +562,6 @@ import initChangesDropdown from './init_changes_dropdown'; case 'projects': new Project(); new ProjectAvatar(); - new GroupName(); switch (path[1]) { case 'compare': new CompareAutocomplete(); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 1c5ca1d3cf9..23040cd9eb8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { .map(tokenKey => ({ icon: `fa-${tokenKey.icon}`, hint: tokenKey.key, - tag: `<${tokenKey.tag}>`, + tag: `:${tokenKey.tag}`, type: tokenKey.type, })); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d65bbc0d808..50d822eba5a 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -175,7 +175,7 @@ GitLabDropdownFilter = (function() { elements.show().removeClass('option-hidden'); } - elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible')); + elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible')); } }; @@ -247,7 +247,7 @@ GitLabDropdown = (function() { currentIndex = -1; - NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; + NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; @@ -637,11 +637,15 @@ GitLabDropdown = (function() { value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; - if (value) { value = value.toString().replace(/'/g, '\\\''); } - - field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); - if (field.length) { - selected = true; + if (value) { + value = value.toString().replace(/'/g, '\\\''); + field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); + if (field.length) { + selected = true; + } + } else { + field = this.dropdown.parent().find(`input[name='${fieldName}']`); + selected = !field.length; } } // Set URL @@ -698,7 +702,7 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.noResults = function() { var html; - return html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>'; + return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>'; }; GitLabDropdown.prototype.rowClicked = function(el) { diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js deleted file mode 100644 index 3e483b69fd2..00000000000 --- a/app/assets/javascripts/group_name.js +++ /dev/null @@ -1,76 +0,0 @@ -import Cookies from 'js-cookie'; -import _ from 'underscore'; - -export default class GroupName { - constructor() { - this.titleContainer = document.querySelector('.js-title-container'); - this.title = this.titleContainer.querySelector('.title'); - - if (this.title) { - this.titleWidth = this.title.offsetWidth; - this.groupTitle = this.titleContainer.querySelector('.group-title'); - this.groups = this.titleContainer.querySelectorAll('.group-path'); - this.toggle = null; - this.isHidden = false; - this.init(); - } - } - - init() { - if (this.groups.length > 0) { - this.groups[this.groups.length - 1].classList.remove('hidable'); - this.toggleHandler(); - window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100)); - } - this.render(); - } - - toggleHandler() { - if (this.titleWidth > this.titleContainer.offsetWidth) { - if (!this.toggle) this.createToggle(); - this.showToggle(); - } else if (this.toggle) { - this.hideToggle(); - } - } - - createToggle() { - this.toggle = document.createElement('button'); - this.toggle.setAttribute('type', 'button'); - this.toggle.className = 'text-expander group-name-toggle'; - this.toggle.setAttribute('aria-label', 'Toggle full path'); - if (Cookies.get('new_nav') === 'true') { - this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>'; - } else { - this.toggle.innerHTML = '...'; - } - this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - if (Cookies.get('new_nav') === 'true') { - this.title.insertBefore(this.toggle, this.groupTitle); - } else { - this.titleContainer.insertBefore(this.toggle, this.title); - } - this.toggleGroups(); - } - - showToggle() { - this.title.classList.add('wrap'); - this.toggle.classList.remove('hidden'); - if (this.isHidden) this.groupTitle.classList.add('hidden'); - } - - hideToggle() { - this.title.classList.remove('wrap'); - this.toggle.classList.add('hidden'); - if (this.isHidden) this.groupTitle.classList.remove('hidden'); - } - - toggleGroups() { - this.isHidden = !this.isHidden; - this.groupTitle.classList.toggle('hidden'); - } - - render() { - this.title.classList.remove('initializing'); - } -} diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index d314f3c4d43..0e8a0519928 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,7 +5,6 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; -import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar { new SubscriptionSelect(); } - getNavHeight() { - const navbarHeight = $('.navbar-gitlab').outerHeight(); - const layoutNavHeight = $('.layout-nav').outerHeight(); - const subNavScroll = $('.sub-nav-scroll').outerHeight(); - return navbarHeight + layoutNavHeight + subNavScroll; - } - setupBulkUpdateActions() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar { this.toggleBulkEditButtonDisabled(enable); this.toggleOtherFiltersDisabled(enable); this.toggleCheckboxDisplay(enable); - - if (enable) { - this.initAffix(); - SidebarHeightManager.init(); - } - } - - initAffix() { - if (!this.$sidebar.hasClass('affix-top')) { - const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight(); - - this.$sidebar.affix({ - offset: { - top: offsetTop, - }, - }); - } } updateSelectedIssuableIds() { diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 5c1ba416a03..d064a2c0024 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav'; }); }); - function applyScrollNavClass() { - const scrollOpacityHeight = 40; - $('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1)); - } - $(() => { - if (Cookies.get('new_nav') === 'true') { - const newNavSidebar = new NewNavSidebar(); - newNavSidebar.bindEvents(); - - initFlyOutNav(); - } + const newNavSidebar = new NewNavSidebar(); + newNavSidebar.bindEvents(); - $(window).on('scroll', _.throttle(applyScrollNavClass, 100)); + initFlyOutNav(); }); }).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 69a6e131b59..0bc31a56684 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -132,6 +132,7 @@ import './project_new'; import './project_select'; import './project_show'; import './project_variables'; +import './projects_dropdown'; import './projects_list'; import './syntax_highlight'; import './render_math'; @@ -143,6 +144,7 @@ import './smart_interval'; import './star'; import './subscription'; import './subscription_select'; +import initBreadcrumbs from './breadcrumb'; import './dispatcher'; @@ -180,6 +182,8 @@ $(function () { var bootstrapBreakpoint = bp.getBreakpointSize(); var fitSidebarForSize; + initBreadcrumbs(); + // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; @@ -249,7 +253,10 @@ $(function () { // Initialize popovers $body.popover({ selector: '[data-toggle="popover"]', - trigger: 'focus' + trigger: 'focus', + // set the viewport to the main content, excluding the navigation bar, so + // the navigation can't overlap the popover + viewport: '.page-with-sidebar' }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 74244faa5d9..b596c4f383f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -4,7 +4,7 @@ import statusCodes from '../../lib/utils/http_status'; import MonitoringService from '../services/monitoring_service'; import GraphGroup from './graph_group.vue'; - import GraphRow from './graph_row.vue'; + import Graph from './graph.vue'; import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; @@ -32,8 +32,8 @@ }, components: { + Graph, GraphGroup, - GraphRow, EmptyState, }, @@ -127,10 +127,10 @@ :key="index" :name="groupData.group" > - <graph-row - v-for="(row, index) in groupData.metrics" + <graph + v-for="(graphData, index) in groupData.metrics" :key="index" - :row-data="row" + :graph-data="graphData" :update-aspect-ratio="updateAspectRatio" :deployment-data="store.deploymentData" /> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 9c785f4ada8..cde2ff7ca2a 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -19,10 +19,6 @@ type: Object, required: true, }, - classType: { - type: String, - required: true, - }, updateAspectRatio: { type: Boolean, required: true, @@ -207,12 +203,11 @@ }, }; </script> + <template> - <div - :class="classType"> - <h5 - class="text-center graph-title"> - {{graphData.title}} + <div class="prometheus-graph"> + <h5 class="text-center graph-title"> + {{graphData.title}} </h5> <div class="prometheus-svg-container" @@ -243,7 +238,7 @@ class="graph-data" :viewBox="innerViewBox" ref="graphData"> - <monitoring-paths + <monitoring-paths v-for="(path, index) in timeSeries" :key="index" :generated-line-path="path.linePath" diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 32c90fda8cc..958f537d31b 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -14,7 +14,7 @@ export default { <div class="panel-heading"> <h4>{{name}}</h4> </div> - <div class="panel-body"> + <div class="panel-body prometheus-graph-group"> <slot /> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/graph_row.vue b/app/assets/javascripts/monitoring/components/graph_row.vue deleted file mode 100644 index bdb9149c3b4..00000000000 --- a/app/assets/javascripts/monitoring/components/graph_row.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script> - import Graph from './graph.vue'; - - export default { - props: { - rowData: { - type: Array, - required: true, - }, - updateAspectRatio: { - type: Boolean, - required: true, - }, - deploymentData: { - type: Array, - required: true, - }, - }, - components: { - Graph, - }, - computed: { - bootstrapClass() { - return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12'; - }, - }, - }; -</script> - -<template> - <div class="prometheus-row row"> - <graph - v-for="(graphData, index) in rowData" - :graph-data="graphData" - :class-type="bootstrapClass" - :key="index" - :update-aspect-ratio="updateAspectRatio" - :deployment-data="deploymentData" - /> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 0a4cdd88044..7592af5878e 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -20,22 +20,6 @@ function normalizeMetrics(metrics) { })); } -function collate(array, rows = 2) { - const collatedArray = []; - let row = []; - array.forEach((value, index) => { - row.push(value); - if ((index + 1) % rows === 0) { - collatedArray.push(row); - row = []; - } - }); - if (row.length > 0) { - collatedArray.push(row); - } - return collatedArray; -} - export default class MonitoringStore { constructor() { this.groups = []; @@ -45,7 +29,7 @@ export default class MonitoringStore { storeMetrics(groups = []) { this.groups = groups.map(group => ({ ...group, - metrics: collate(normalizeMetrics(sortMetrics(group.metrics))), + metrics: normalizeMetrics(sortMetrics(group.metrics)), })); } @@ -54,12 +38,6 @@ export default class MonitoringStore { } getMetricsCount() { - let metricsCount = 0; - this.groups.forEach((group) => { - group.metrics.forEach((metric) => { - metricsCount = metricsCount += metric.length; - }); - }); - return metricsCount; + return this.groups.reduce((count, group) => count + group.metrics.length, 0); } } diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index 05e3f33f5ed..709a5d33b9f 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -63,7 +63,7 @@ export default class NewNavSidebar { if (breakpoint === 'sm' || breakpoint === 'md') { this.toggleCollapsedSidebar(true); } else if (breakpoint === 'lg') { - const collapse = Cookies.get('sidebar_collapsed') === 'true'; + const collapse = this.$sidebar.hasClass('sidebar-icons-only'); this.toggleCollapsedSidebar(collapse); } } diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index d7e3ab42f00..fe6602259e2 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -53,10 +53,6 @@ import Cookies from 'js-cookie'; return _this.changeProject($(e.currentTarget).val()); }; })(this)); - return $('.js-projects-dropdown-toggle').on('click', function(e) { - e.preventDefault(); - return $('.js-projects-dropdown').select2('open'); - }); }; Project.prototype.changeProject = function(url) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 1b4ed6be90a..fb01390f91c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button'; (function() { this.ProjectSelect = (function() { function ProjectSelect() { - $('.js-projects-dropdown-toggle').each(function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return $dropdown.glDropdown({ - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'] - }, - data: function(term, callback) { - var finalCallback, projectsCallback; - var orderBy = $dropdown.data('order-by'); - finalCallback = function(projects) { - return callback(projects); - }; - if (this.includeGroups) { - projectsCallback = function(projects) { - var groupsCallback; - groupsCallback = function(groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (this.groupId) { - return Api.groupProjects(this.groupId, term, projectsCallback); - } else { - return Api.projects(term, { order_by: orderBy }, projectsCallback); - } - }, - url: function(project) { - return project.web_url; - }, - text: function(project) { - return project.name_with_namespace; - } - }); - }); $('.ajax-project-select').each(function(i, select) { var placeholder; this.groupId = $(select).data('group-id'); diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue new file mode 100644 index 00000000000..7606605be32 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -0,0 +1,157 @@ +<script> +import bs from '../../breakpoints'; +import eventHub from '../event_hub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + +import projectsListFrequent from './projects_list_frequent.vue'; +import projectsListSearch from './projects_list_search.vue'; + +import search from './search.vue'; + +export default { + components: { + search, + loadingIcon, + projectsListFrequent, + projectsListSearch, + }, + props: { + currentProject: { + type: Object, + required: true, + }, + store: { + type: Object, + required: true, + }, + service: { + type: Object, + required: true, + }, + }, + data() { + return { + isLoadingProjects: false, + isFrequentsListVisible: false, + isSearchListVisible: false, + isLocalStorageFailed: false, + isSearchFailed: false, + searchQuery: '', + }; + }, + computed: { + frequentProjects() { + return this.store.getFrequentProjects(); + }, + searchProjects() { + return this.store.getSearchedProjects(); + }, + }, + methods: { + toggleFrequentProjectsList(state) { + this.isLoadingProjects = !state; + this.isSearchListVisible = !state; + this.isFrequentsListVisible = state; + }, + toggleSearchProjectsList(state) { + this.isLoadingProjects = !state; + this.isFrequentsListVisible = !state; + this.isSearchListVisible = state; + }, + toggleLoader(state) { + this.isFrequentsListVisible = !state; + this.isSearchListVisible = !state; + this.isLoadingProjects = state; + }, + fetchFrequentProjects() { + const screenSize = bs.getBreakpointSize(); + if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) { + this.toggleSearchProjectsList(true); + } else { + this.toggleLoader(true); + this.isLocalStorageFailed = false; + const projects = this.service.getFrequentProjects(); + if (projects) { + this.toggleFrequentProjectsList(true); + this.store.setFrequentProjects(projects); + } else { + this.isLocalStorageFailed = true; + this.toggleFrequentProjectsList(true); + this.store.setFrequentProjects([]); + } + } + }, + fetchSearchedProjects(searchQuery) { + this.searchQuery = searchQuery; + this.toggleLoader(true); + this.service.getSearchedProjects(this.searchQuery) + .then(res => res.json()) + .then((results) => { + this.toggleSearchProjectsList(true); + this.store.setSearchedProjects(results); + }) + .catch(() => { + this.isSearchFailed = true; + this.toggleSearchProjectsList(true); + }); + }, + logCurrentProjectAccess() { + this.service.logProjectAccess(this.currentProject); + }, + handleSearchClear() { + this.searchQuery = ''; + this.toggleFrequentProjectsList(true); + this.store.clearSearchedProjects(); + }, + handleSearchFailure() { + this.isSearchFailed = true; + this.toggleSearchProjectsList(true); + }, + }, + created() { + if (this.currentProject.id) { + this.logCurrentProjectAccess(); + } + + eventHub.$on('dropdownOpen', this.fetchFrequentProjects); + eventHub.$on('searchProjects', this.fetchSearchedProjects); + eventHub.$on('searchCleared', this.handleSearchClear); + eventHub.$on('searchFailed', this.handleSearchFailure); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.fetchFrequentProjects); + eventHub.$off('searchProjects', this.fetchSearchedProjects); + eventHub.$off('searchCleared', this.handleSearchClear); + eventHub.$off('searchFailed', this.handleSearchFailure); + }, +}; +</script> + +<template> + <div> + <search/> + <loading-icon + class="loading-animation prepend-top-20" + size="2" + v-if="isLoadingProjects" + :label="s__('ProjectsDropdown|Loading projects')" + /> + <div + class="section-header" + v-if="isFrequentsListVisible" + > + {{ s__('ProjectsDropdown|Frequently visited') }} + </div> + <projects-list-frequent + v-if="isFrequentsListVisible" + :local-storage-failed="isLocalStorageFailed" + :projects="frequentProjects" + /> + <projects-list-search + v-if="isSearchListVisible" + :search-failed="isSearchFailed" + :matcher="searchQuery" + :projects="searchProjects" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue new file mode 100644 index 00000000000..093554cd0bc --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -0,0 +1,57 @@ +<script> +import { s__ } from '../../locale'; +import projectsListItem from './projects_list_item.vue'; + +export default { + components: { + projectsListItem, + }, + props: { + projects: { + type: Array, + required: true, + }, + localStorageFailed: { + type: Boolean, + required: true, + }, + }, + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.localStorageFailed ? + s__('ProjectsDropdown|This feature requires browser localStorage support') : + s__('ProjectsDropdown|Projects you visit often will appear here'); + }, + }, +}; +</script> + +<template> + <div + class="projects-list-frequent-container" + > + <ul + class="list-unstyled" + > + <li + class="section-empty" + v-if="isListEmpty" + > + {{listEmptyMessage}} + </li> + <projects-list-item + v-else + v-for="(project, index) in projects" + :key="index" + :project-id="project.id" + :project-name="project.name" + :namespace="project.namespace" + :web-url="project.webUrl" + :avatar-url="project.avatarUrl" + /> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue new file mode 100644 index 00000000000..fe5179de206 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -0,0 +1,96 @@ +<script> +import identicon from '../../vue_shared/components/identicon.vue'; + +export default { + components: { + identicon, + }, + props: { + matcher: { + type: String, + required: false, + }, + projectId: { + type: Number, + required: true, + }, + projectName: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + avatarUrl: { + required: true, + validator(value) { + return value === null || typeof value === 'string'; + }, + }, + }, + computed: { + hasAvatar() { + return this.avatarUrl !== null; + }, + highlightedProjectName() { + if (this.matcher) { + const matcherRegEx = new RegExp(this.matcher, 'gi'); + const matches = this.projectName.match(matcherRegEx); + + if (matches && matches.length > 0) { + return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + } + } + return this.projectName; + }, + }, +}; +</script> + +<template> + <li + class="projects-list-item-container" + > + <a + class="clearfix" + :href="webUrl" + > + <div + class="project-item-avatar-container" + > + <img + v-if="hasAvatar" + class="avatar s32" + :src="avatarUrl" + /> + <identicon + v-else + size-class="s32" + :entity-id=projectId + :entity-name="projectName" + /> + </div> + <div + class="project-item-metadata-container" + > + <div + class="project-title" + :title="projectName" + v-html="highlightedProjectName" + > + </div> + <div + class="project-namespace" + :title="namespace" + > + {{namespace}} + </div> + </div> + </a> + </li> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue new file mode 100644 index 00000000000..fa5efef2919 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -0,0 +1,63 @@ +<script> +import { s__ } from '../../locale'; +import projectsListItem from './projects_list_item.vue'; + +export default { + components: { + projectsListItem, + }, + props: { + matcher: { + type: String, + required: true, + }, + projects: { + type: Array, + required: true, + }, + searchFailed: { + type: Boolean, + required: true, + }, + }, + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.searchFailed ? + s__('ProjectsDropdown|Something went wrong on our end.') : + s__('ProjectsDropdown|No projects matched your query'); + }, + }, +}; +</script> + +<template> + <div + class="projects-list-search-container" + > + <ul + class="list-unstyled" + > + <li + v-if="isListEmpty" + :class="{ 'section-failure': searchFailed }" + class="section-empty" + > + {{ listEmptyMessage }} + </li> + <projects-list-item + v-else + v-for="(project, index) in projects" + :key="index" + :project-id="project.id" + :project-name="project.name" + :namespace="project.namespace" + :web-url="project.webUrl" + :avatar-url="project.avatarUrl" + :matcher="matcher" + /> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue new file mode 100644 index 00000000000..b71997234e5 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -0,0 +1,64 @@ +<script> +import _ from 'underscore'; +import eventHub from '../event_hub'; + +export default { + data() { + return { + searchQuery: '', + }; + }, + watch: { + searchQuery() { + this.handleInput(); + }, + }, + methods: { + setFocus() { + this.$refs.search.focus(); + }, + emitSearchEvents() { + if (this.searchQuery) { + eventHub.$emit('searchProjects', this.searchQuery); + } else { + eventHub.$emit('searchCleared'); + } + }, + /** + * Callback function within _.debounce is intentionally + * kept as ES5 `function() {}` instead of ES6 `() => {}` + * as it otherwise messes up function context + * and component reference is no longer accessible via `this` + */ + // eslint-disable-next-line func-names + handleInput: _.debounce(function () { + this.emitSearchEvents(); + }, 500), + }, + mounted() { + eventHub.$on('dropdownOpen', this.setFocus); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.setFocus); + }, +}; +</script> + +<template> + <div + class="search-input-container hidden-xs" + > + <input + type="search" + class="form-control" + ref="search" + v-model="searchQuery" + :placeholder="s__('ProjectsDropdown|Search projects')" + /> + <i + v-if="!searchQuery" + class="search-icon fa fa-fw fa-search" + aria-hidden="true" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js new file mode 100644 index 00000000000..8937097184c --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/constants.js @@ -0,0 +1,10 @@ +export const FREQUENT_PROJECTS = { + MAX_COUNT: 20, + LIST_COUNT_DESKTOP: 5, + LIST_COUNT_MOBILE: 3, + ELIGIBLE_FREQUENCY: 3, +}; + +export const HOUR_IN_MS = 3600000; + +export const STORAGE_KEY = 'frequent-projects'; diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/projects_dropdown/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js new file mode 100644 index 00000000000..2660da3c558 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -0,0 +1,68 @@ +import Vue from 'vue'; + +import Translate from '../vue_shared/translate'; +import eventHub from './event_hub'; +import ProjectsService from './service/projects_service'; +import ProjectsStore from './store/projects_store'; + +import projectsDropdownApp from './components/app.vue'; + +Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => { + const el = document.getElementById('js-projects-dropdown'); + const navEl = document.getElementById('nav-projects-dropdown'); + + // Don't do anything if element doesn't exist (No projects dropdown) + // This is for when the user accesses GitLab without logging in + if (!el || !navEl) { + return; + } + + $(navEl).on('show.bs.dropdown', (e) => { + const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu'); + dropdownEl.one('transitionend', () => { + eventHub.$emit('dropdownOpen'); + }); + }); + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + projectsDropdownApp, + }, + data() { + const dataset = this.$options.el.dataset; + const store = new ProjectsStore(); + const service = new ProjectsService(dataset.userName); + + const project = { + id: Number(dataset.projectId), + name: dataset.projectName, + namespace: dataset.projectNamespace, + webUrl: dataset.projectWebUrl, + avatarUrl: dataset.projectAvatarUrl || null, + lastAccessedOn: Date.now(), + }; + + return { + store, + service, + state: store.state, + currentUserName: dataset.userName, + currentProject: project, + }; + }, + render(createElement) { + return createElement('projects-dropdown-app', { + props: { + currentUserName: this.currentUserName, + currentProject: this.currentProject, + store: this.store, + service: this.service, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js new file mode 100644 index 00000000000..fad956b4c26 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -0,0 +1,132 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import bp from '../../breakpoints'; +import Api from '../../api'; +import AccessorUtilities from '../../lib/utils/accessor'; + +import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants'; + +Vue.use(VueResource); + +export default class ProjectsService { + constructor(currentUserName) { + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + this.currentUserName = currentUserName; + this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`; + this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath)); + } + + getSearchedProjects(searchQuery) { + return this.projectsPath.get({ + simple: false, + per_page: 20, + membership: !!gon.current_user_id, + order_by: 'last_activity_at', + search: searchQuery, + }); + } + + getFrequentProjects() { + if (this.isLocalStorageAvailable) { + return this.getTopFrequentProjects(); + } + return null; + } + + logProjectAccess(project) { + let matchFound = false; + let storedFrequentProjects; + + if (this.isLocalStorageAvailable) { + const storedRawProjects = localStorage.getItem(this.storageKey); + + // Check if there's any frequent projects list set + if (!storedRawProjects) { + // No frequent projects list set, set one up. + storedFrequentProjects = []; + storedFrequentProjects.push({ ...project, frequency: 1 }); + } else { + // Check if project is already present in frequents list + // When found, update metadata of it. + storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => { + if (projectItem.id === project.id) { + matchFound = true; + const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS; + const updatedProject = { + ...project, + frequency: projectItem.frequency, + lastAccessedOn: projectItem.lastAccessedOn, + }; + + // Check if duration since last access of this project + // is over an hour + if (diff > 1) { + return { + ...updatedProject, + frequency: updatedProject.frequency + 1, + lastAccessedOn: Date.now(), + }; + } + + return { + ...updatedProject, + }; + } + + return projectItem; + }); + + // Check whether currently logged project is present in frequents list + if (!matchFound) { + // We always keep size of frequents collection to 20 projects + // out of which only 5 projects with + // highest value of `frequency` and most recent `lastAccessedOn` + // are shown in projects dropdown + if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) { + storedFrequentProjects.shift(); // Remove an item from head of array + } + + storedFrequentProjects.push({ ...project, frequency: 1 }); + } + } + + localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects)); + } + } + + getTopFrequentProjects() { + const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey)); + let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP; + + if (!storedFrequentProjects) { + return []; + } + + if (bp.getBreakpointSize() === 'sm' || + bp.getBreakpointSize() === 'xs') { + frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE; + } + + const frequentProjects = storedFrequentProjects + .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY); + + // Sort all frequent projects in decending order of frequency + // and then by lastAccessedOn with recent most first + frequentProjects.sort((projectA, projectB) => { + if (projectA.frequency < projectB.frequency) { + return 1; + } else if (projectA.frequency > projectB.frequency) { + return -1; + } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) { + return 1; + } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) { + return -1; + } + + return 0; + }); + + return _.first(frequentProjects, frequentProjectsCount); + } +} diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js new file mode 100644 index 00000000000..ffefbe693f4 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/store/projects_store.js @@ -0,0 +1,33 @@ +export default class ProjectsStore { + constructor() { + this.state = {}; + this.state.frequentProjects = []; + this.state.searchedProjects = []; + } + + setFrequentProjects(rawProjects) { + this.state.frequentProjects = rawProjects; + } + + getFrequentProjects() { + return this.state.frequentProjects; + } + + setSearchedProjects(rawProjects) { + this.state.searchedProjects = rawProjects.map(rawProject => ({ + id: rawProject.id, + name: rawProject.name, + namespace: rawProject.name_with_namespace, + webUrl: rawProject.web_url, + avatarUrl: rawProject.avatar_url, + })); + } + + getSearchedProjects() { + return this.state.searchedProjects; + } + + clearSearchedProjects() { + this.state.searchedProjects = []; + } +} diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 4c87d46c96e..a4eae135403 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; -import SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager'; }; Sidebar.prototype.addEventListeners = function() { - SidebarHeightManager.init(); const $document = $(document); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 6fd5345a0a6..003a15592f3 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -363,7 +363,7 @@ restoreMenu() { var html; - html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; + html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; return this.dropdownContent.html(html); } diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js deleted file mode 100644 index 2752fe2b911..00000000000 --- a/app/assets/javascripts/sidebar_height_manager.js +++ /dev/null @@ -1,37 +0,0 @@ -import _ from 'underscore'; -import Cookies from 'js-cookie'; - -export default { - init() { - if (!this.initialized) { - if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return; - - this.$window = $(window); - this.$rightSidebar = $('.js-right-sidebar'); - this.$navHeight = $('.navbar-gitlab').outerHeight() + - $('.layout-nav').outerHeight() + - $('.sub-nav-scroll').outerHeight(); - - const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20); - const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200); - - this.$window.on('scroll', throttledSetSidebarHeight); - this.$window.on('resize', debouncedSetSidebarHeight); - this.initialized = true; - } - }, - - setSidebarHeight() { - const currentScrollDepth = window.pageYOffset || 0; - const diff = this.$navHeight - currentScrollDepth; - - if (diff > 0) { - const newSidebarHeight = window.innerHeight - diff; - this.$rightSidebar.outerHeight(newSidebarHeight); - this.sidebarHeightIsCustom = true; - } else if (this.sidebarHeightIsCustom) { - this.$rightSidebar.outerHeight('100%'); - this.sidebarHeightIsCustom = false; - } - }, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index c05a76a3b4a..aaca42e3ebc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -75,18 +75,20 @@ export default { class="btn btn-small inline"> Check out branch </a> - <span class="dropdown inline prepend-left-10"> + <span class="dropdown prepend-left-10"> <a - class="btn btn-xs dropdown-toggle" + class="btn btn-small inline dropdown-toggle" data-toggle="dropdown" aria-label="Download as" role="button"> <i class="fa fa-download" - aria-hidden="true" /> + aria-hidden="true"> + </i> <i class="fa fa-caret-down" - aria-hidden="true" /> + aria-hidden="true"> + </i> </a> <ul class="dropdown-menu dropdown-menu-align-right"> <li> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 0edd820743f..7cf2e029cf6 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -9,6 +9,11 @@ export default { type: String, required: true, }, + sizeClass: { + type: String, + required: false, + default: 's40', + }, }, computed: { /** @@ -38,7 +43,8 @@ export default { <template> <div - class="avatar s40 identicon" + class="avatar identicon" + :class="sizeClass" :style="identiconStyles"> {{identiconTitle}} </div> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 68a51c5a461..a85051642dd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -21,6 +21,7 @@ .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } .append-bottom-0 { margin-bottom: 0; } +.append-bottom-5 { margin-bottom: 5px; } .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } .append-bottom-20 { margin-bottom: 20px; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 336ac096d60..d4a1bb8402c 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -163,12 +163,6 @@ } } - &.dropdown-menu-empty-link { - &.is-focused { - background-color: $dropdown-empty-row-bg; - } - } - &.dropdown-menu-user-link { line-height: 16px; } @@ -256,6 +250,13 @@ @include dropdown-link; } + .dropdown-menu-empty-item a { + &:hover, + &:focus { + background-color: transparent; + } + } + .dropdown-header { color: $gl-text-color-secondary; font-size: 13px; @@ -766,6 +767,7 @@ box-shadow: none; padding: 8px 16px; text-align: left; + white-space: normal; width: 100%; // make sure the text color is not overriden @@ -799,6 +801,13 @@ } } } + + &.dropdown-menu-empty-item a { + &:hover, + &:focus { + background-color: transparent; + } + } } &.dropdown-menu-selectable { @@ -828,4 +837,154 @@ } } +@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.js-namespace-select + '); + +header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu { + padding: 0; + + @media (max-width: $screen-xs-max) { + display: table; + left: -50px; + min-width: 300px; + } +} + +.projects-dropdown-container { + display: flex; + flex-direction: row; + width: 500px; + height: 334px; + + .project-dropdown-sidebar, + .project-dropdown-content { + padding: 8px 0; + } + + .loading-animation { + color: $almost-black; + } + + .project-dropdown-sidebar { + width: 30%; + border-right: 1px solid $border-color; + } + + .project-dropdown-content { + position: relative; + width: 70%; + } + + @media (max-width: $screen-xs-max) { + flex-direction: column; + width: 100%; + height: auto; + flex: 1; + + .project-dropdown-sidebar, + .project-dropdown-content { + width: 100%; + } + + .project-dropdown-sidebar { + border-bottom: 1px solid $border-color; + border-right: 0; + } + } +} + +.projects-dropdown-container { + .projects-list-frequent-container, + .projects-list-search-container, { + padding: 8px 0; + overflow-y: auto; + } + + .section-header, + .projects-list-frequent-container li.section-empty, + .projects-list-search-container li.section-empty { + padding: 0 15px; + } + + .section-header, + .projects-list-frequent-container li.section-empty, + .projects-list-search-container li.section-empty { + color: $gl-text-color-secondary; + font-size: $gl-font-size; + } + + .projects-list-frequent-container, + .projects-list-search-container { + li.section-empty.section-failure { + color: $callout-danger-color; + } + } + + .search-input-container { + position: relative; + padding: 4px $gl-padding; + + .search-icon { + position: absolute; + top: 13px; + right: 25px; + color: $md-area-border; + } + } + + .section-header { + font-weight: 700; + margin-top: 8px; + } + + .projects-list-search-container { + height: 284px; + } + + @media (max-width: $screen-xs-max) { + .projects-list-frequent-container { + width: auto; + height: auto; + padding-bottom: 0; + } + } +} + +.projects-list-item-container { + .project-item-avatar-container + .project-item-metadata-container { + float: left; + } + + .project-title, + .project-namespace { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + .project-item-avatar-container .avatar { + border-color: $md-area-border; + } + } + + .project-title { + font-size: $gl-font-size; + font-weight: 400; + line-height: 16px; + } + + .project-namespace { + margin-top: 4px; + font-size: 12px; + line-height: 12px; + color: $gl-text-color-secondary; + } + + @media (max-width: $screen-xs-max) { + .project-item-metadata-container { + float: none; + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 35bd97980e2..b00a2d053e2 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -105,12 +105,11 @@ header { top: -3px; font-size: 10px; } + } + .user-counter { svg { - position: relative; - top: 2px; - height: 17px; - // hack to get SVG to line up with FA icons + height: 16px; width: 23px; fill: currentColor; } @@ -325,12 +324,12 @@ header { li { .badge { position: inherit; - top: -8px; font-weight: $gl-font-weight-normal; - margin-left: -11px; + margin-left: -6px; font-size: 11px; color: $white-light; - padding: 1px 5px 2px; + padding: 0 5px; + line-height: 12px; border-radius: 7px; box-shadow: 0 1px 0 rgba($gl-header-color, .2); diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e4f82fe091a..6c14e8b97e0 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -267,11 +267,13 @@ // TODO: change global style .ajax-project-dropdown, +.ajax-users-dropdown, body[data-page="projects:edit"] #select2-drop, body[data-page="projects:new"] #select2-drop, body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, +body[data-page="admin:groups:show"] #select2-drop, body[data-page="projects:issues:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 01fffa717e9..88b08998dfd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -177,13 +177,14 @@ $row-hover: $blue-25; $row-hover-border: $blue-100; $progress-color: #c0392b; $header-height: 50px; +$new-navbar-height: 40px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; $container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; -$border-radius-default: 3px; +$border-radius-default: 4px; $settings-icon-size: 18px; $provider-btn-not-active-color: $blue-500; $link-underline-blue: $blue-500; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index b711bd12c73..e4b52ab480d 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -1,16 +1,23 @@ @import "framework/variables"; @import 'framework/tw_bootstrap_variables'; @import "bootstrap/variables"; +@import "framework/mixins"; + +.content-wrapper.page-with-new-nav { + margin-top: $new-navbar-height; +} header.navbar-gitlab-new { color: $white-light; background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; + min-height: $new-navbar-height; .header-content { display: -webkit-flex; display: flex; padding-left: 0; + min-height: $new-navbar-height; .title-container { display: -webkit-flex; @@ -38,20 +45,13 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; align-items: center; - padding-right: $gl-padding; - padding-left: $gl-padding; - margin-left: -$gl-padding; - - @media (min-width: $screen-sm-min) { - padding-right: $gl-padding; - padding-left: $gl-padding; - } + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: $border-radius-default; svg { - margin-top: -3px; - @media (min-width: $screen-sm-min) { - margin-right: 10px; + margin-right: 8px; } } @@ -60,7 +60,7 @@ header.navbar-gitlab-new { svg { width: 55px; - height: 15px; + height: 14px; margin: 0; fill: $white-light; } @@ -68,9 +68,7 @@ header.navbar-gitlab-new { &:hover, &:focus { - .logo-text svg { - fill: $tanuki-yellow; - } + background-color: rgba($indigo-200, .2); } } } @@ -90,6 +88,20 @@ header.navbar-gitlab-new { right: 0; } } + + &.menu-expanded { + @media (max-width: $screen-xs-max) { + .title-container, + .header-logo, { + display: none; + } + } + } + } + + .dropdown-bold-header { + color: $gl-text-color-secondary; + font-size: 12px; } .navbar-collapse { @@ -98,14 +110,10 @@ header.navbar-gitlab-new { box-shadow: 0; @media (max-width: $screen-xs-max) { - margin-left: -$gl-padding; + margin-left: -8px; margin-right: -10px; } - .dropdown-bold-header { - color: initial; - } - .nav { > li:not(.hidden-xs) a { @media (max-width: $screen-xs-max) { @@ -119,7 +127,7 @@ header.navbar-gitlab-new { .container-fluid { .navbar-toggle { min-width: 45px; - padding: 6px $gl-padding; + padding: 4px $gl-padding; margin-right: -7px; font-size: 14px; text-align: center; @@ -156,31 +164,90 @@ header.navbar-gitlab-new { } > a { - background: none; will-change: color; + margin: 4px 2px; + padding: 6px 8px; + color: $indigo-200; + height: 32px; + + @media (max-width: $screen-xs-max) { + padding: 0; + } + + svg { + fill: $indigo-200; + } &.header-user-dropdown-toggle { + margin-left: 2px; + .header-user-avatar { border-color: $indigo-200; + margin-right: 0; } } + } - &:hover, - &:focus { - color: $white-light; - opacity: 1; + .header-new-dropdown-toggle { + margin-right: 0; + } - > svg { - fill: $white-light; - } + > a:hover, + > a:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; + + @media (min-width: $screen-sm-min) { + background-color: rgba($indigo-200, .2); + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; - } + svg { + fill: currentColor; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; } } } + + .impersonated-user, + .impersonated-user:hover { + margin-right: 1px; + background-color: $white-light; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + svg { + fill: $indigo-900; + } + } + + .impersonation-btn, + .impersonation-btn:hover { + background-color: $white-light; + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + i { + color: $orange-500; + font-size: 20px; + } + } + + &.active > a, + &.dropdown.open > a { + color: $indigo-900; + background-color: $white-light; + + svg { + fill: currentColor; + } + } } } } @@ -188,45 +255,76 @@ header.navbar-gitlab-new { .navbar-sub-nav { display: -webkit-flex; display: flex; - margin-bottom: 0; + margin: 0 0 0 6px; color: $indigo-200; - > li { - > a:hover, - > a:focus { - box-shadow: inset 0 -3px 0 rgba($indigo-200, .4); - text-decoration: none; - outline: 0; - color: $white-light; - } + .dropdown-chevron { + position: relative; + top: -1px; + font-size: 10px; + } +} - &.active > a { - box-shadow: inset 0 -3px 0 $indigo-500; - color: $white-light; - font-weight: $gl-font-weight-bold; - } +.navbar-gitlab-new { + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + text-decoration: none; + outline: 0; + color: $white-light; + background-color: rgba($indigo-200, .2); - > a { - display: block; - padding: 16px 10px; - font-size: 13px; - color: currentColor; - box-shadow: inset 0 0 0 transparent; - will-change: box-shadow; - transition: box-shadow 0.15s; + svg { + fill: currentColor; + } + } - @media (min-width: $screen-sm-min) { - padding: 15px $gl-padding; - font-size: 14px; + &.active > a, + &.dropdown.open > a { + color: $indigo-900; + background-color: $white-light; + + svg { + fill: currentColor; + } + } + + > a { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: $border-radius-default; + height: 32px; + font-weight: $gl-font-weight-bold; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($indigo-200, .2); + margin: 8px; } } } +} - .dropdown-chevron { - position: relative; - top: -1px; - font-size: 10px; - } +.admin-icon i { + font-size: 18px; +} + +.caret-down { + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; } .header-user .dropdown-menu-nav, @@ -235,10 +333,14 @@ header.navbar-gitlab-new { } .search { + margin: 4px 8px 0; + form { + height: 32px; border: 0; + border-radius: $border-radius-default; background-color: rgba($indigo-200, .2); - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { background-color: rgba($indigo-200, .3); @@ -247,31 +349,50 @@ header.navbar-gitlab-new { } &.search-active form { - background-color: rgba($indigo-200, .3); + background-color: $white-light; box-shadow: none; + + .search-input { + color: $gl-text-color; + transition: color ease-in-out 0.15s; + } + + .search-input::placeholder { + color: $gl-text-color-tertiary; + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: $gl-text-color-tertiary; + transition: color ease-in-out 0.15s; + } + } } .search-input { color: $white-light; background: none; + transition: color ease-in-out 0.15s; } .search-input::placeholder { color: rgba($indigo-200, .8); + transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; color: $indigo-100; background-color: rgba($indigo-200, .1); - transition: color 0.15s; will-change: color; margin: -4px 4px -4px -4px; line-height: 25px; padding: 4px 8px; border-radius: 2px 0 0 2px; border-right: 1px solid $indigo-800; - height: 34px; + height: 32px; + transition: border-color ease-in-out 0.15s; } .search-input-wrap { @@ -283,8 +404,9 @@ header.navbar-gitlab-new { &.search-active { .location-badge { - color: $white-light; - background-color: rgba($indigo-200, .2); + color: $gl-text-color; + background-color: $nav-badge-bg; + border-color: $border-color; } .search-input-wrap { @@ -301,109 +423,38 @@ header.navbar-gitlab-new { .breadcrumbs { display: flex; - min-height: 61px; + min-height: 48px; color: $gl-text-color; - border-bottom: 1px solid $border-color; - - .dropdown-toggle-caret { - position: relative; - top: -1px; - padding: 0 5px; - color: $gl-text-color-secondary; - font-size: 10px; - line-height: 1; - background: none; - border: 0; - - &:focus { - outline: 0; - } - } - - // TODO: fallback to global style - .dropdown-menu { - .divider { - margin: 6px 0; - } - - li { - padding: 0 1px; - - a { - border-radius: 0; - padding: 8px 16px; - - &.is-focused, - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .breadcrumbs-container { + display: -webkit-flex; display: flex; width: 100%; position: relative; + padding-top: $gl-padding; + padding-bottom: $gl-padding; align-items: center; - - .dropdown-menu-projects { - margin-top: -$gl-padding; - margin-left: $gl-padding; - } + border-bottom: 1px solid $border-color; } .breadcrumbs-links { + -webkit-flex: 1; flex: 1; min-width: 0; align-self: center; - color: $gl-text-color-quaternary; - - a { - color: $gl-text-color-secondary; - - &:not(:first-child), - &.group-path { - margin-left: 4px; - } - - &:not(:last-of-type), - &.group-path { - margin-right: 3px; - } - } - - .title { - display: inline-block; - - > a { - &:last-of-type:not(:first-child) { - font-weight: $gl-font-weight-bold; - } - } - } + color: $gl-text-color-secondary; .avatar-tile { - margin-right: 5px; + margin-right: 4px; border: 1px solid $border-color; border-radius: 50%; vertical-align: sub; - - &.identicon { - float: left; - width: 16px; - height: 16px; - margin-top: 2px; - font-size: 10px; - } } .text-expander { - margin-left: 4px; - margin-right: 4px; + margin-left: 0; + margin-right: 2px; > i { position: relative; @@ -412,37 +463,52 @@ header.navbar-gitlab-new { } } -.breadcrumbs-extra { +.breadcrumbs-list { + display: -webkit-flex; display: flex; - flex: 0 0 auto; - margin-left: auto; -} - -.breadcrumbs-sub-title { - margin: 2px 0; - font-size: 16px; - font-weight: $gl-font-weight-normal; - line-height: 1; - - ul { - margin: 0; - } + flex-wrap: wrap; + margin-bottom: 0; + line-height: 16px; - li { - display: inline-block; + > li { + display: flex; + align-items: center; + position: relative; &:not(:last-child) { - &::after { - content: "/"; - margin: 0 2px 0 5px; - color: rgba($black, .65); - } + margin-right: 20px; } - &:last-child a { - font-weight: $gl-font-weight-bold; + > a { + font-size: 12px; + color: currentColor; } } +} + +.breadcrumb-item-text { + @include str-truncated(128px); +} + +.breadcrumbs-list-angle { + position: absolute; + right: -12px; + top: 50%; + color: $gl-text-color-tertiary; + transform: translateY(-50%); +} + +.breadcrumbs-extra { + display: flex; + flex: 0 0 auto; + margin-left: auto; +} + +.breadcrumbs-sub-title { + margin: 0; + font-size: 12px; + font-weight: 600; + line-height: 1; a { color: $gl-text-color; @@ -458,3 +524,14 @@ header.navbar-gitlab-new { } } } + +.btn-sign-in { + margin-top: 3px; + background-color: $indigo-100; + color: $indigo-900; + font-weight: $gl-font-weight-bold; + + &:hover { + background-color: $white-light; + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index f624b130e19..fd5e344d8c9 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px; // Override position: absolute .right-sidebar { position: fixed; - height: calc(100% - #{$header-height}); + height: calc(100% - #{$new-navbar-height}); } .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { @@ -45,7 +45,6 @@ $new-sidebar-collapsed-width: 50px; margin-right: 2px; a { - border-bottom: 1px solid $border-color; font-weight: $gl-font-weight-bold; display: flex; align-items: center; @@ -93,7 +92,7 @@ $new-sidebar-collapsed-width: 50px; z-index: 400; width: $new-sidebar-width; transition: left $sidebar-transition-duration; - top: $header-height; + top: $new-navbar-height; bottom: 0; left: 0; background-color: $gray-normal; @@ -189,7 +188,7 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .nav-sidebar { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } .sidebar-sub-level-items { @@ -389,6 +388,10 @@ $new-sidebar-collapsed-width: 50px; } } + .nav-icon-container { + margin-right: 0; + } + .toggle-sidebar-button { width: $new-sidebar-collapsed-width - 2px; padding: 16px 18px; @@ -453,7 +456,7 @@ $new-sidebar-collapsed-width: 50px; // Make issue boards full-height now that sub-nav is gone .boards-list { - height: calc(100vh - #{$header-height}); + height: calc(100vh - #{$new-navbar-height}); @media (min-width: $screen-sm-min) { height: 475px; // Needed for PhantomJS @@ -464,7 +467,7 @@ $new-sidebar-collapsed-width: 50px; } .with-performance-bar .boards-list { - height: calc(100vh - #{$header-height} - #{$performance-bar-height}); + height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0f3074076ce..314dd2d1a21 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -440,6 +440,7 @@ &.right-sidebar { top: 0; bottom: 0; + height: 100%; } .issuable-sidebar-header { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c051d37aad6..587a202d6dd 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -141,17 +141,17 @@ display: inline-block; background: $white-light; color: $gl-text-color-secondary; - padding: 0 5px; + padding: 0 4px; cursor: pointer; border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; - font-size: $gl-font-size; + font-size: 12px; line-height: $gl-font-size; outline: none; &.open { - background: $gray-light; + background-color: darken($gray-light, 10%); box-shadow: inset 0 0 2px rgba($black, 0.2); } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 8cbf0ec6180..a7acaf6c728 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -578,12 +578,12 @@ @media (min-width: $screen-sm-min) { position: -webkit-sticky; position: sticky; - top: 34px; + top: 24px; background-color: $white-light; z-index: 190; &.diff-files-changed-merge-request { - top: 84px; + top: 76px; } + .files, @@ -614,6 +614,14 @@ } } +@media (min-width: $screen-sm-min) { + .with-performance-bar { + .diff-files-changed.diff-files-changed-merge-request { + top: 76px + $performance-bar-height; + } + } +} + .diff-file-changes { width: 450px; z-index: 150; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a52ac0d53e7..9362d80d4e6 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -227,6 +227,26 @@ margin-top: 20px; } +.prometheus-graph-group { + display: flex; + flex-wrap: wrap; + padding: $gl-padding / 2; +} + +.prometheus-graph { + flex: 1 0 auto; + min-width: 450px; + padding: $gl-padding / 2; + + h5 { + font-size: 16px; + } + + @media (max-width: $screen-sm-max) { + min-width: 100%; + } +} + .prometheus-svg-container { position: relative; height: 0; @@ -297,9 +317,3 @@ } } } - -.prometheus-row { - h5 { - font-size: 16px; - } -} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6523376ccc3..d8a15faf7e9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -220,7 +220,7 @@ .right-sidebar { position: absolute; - top: $header-height; + top: $new-navbar-height; bottom: 0; right: 0; transition: width .3s; @@ -230,7 +230,7 @@ .issuable-sidebar { width: calc(100% + 100px); - height: calc(100% - #{$header-height}); + height: calc(100% - #{$new-navbar-height}); overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; @@ -479,10 +479,10 @@ } .with-performance-bar .right-sidebar { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; .issuable-sidebar { - height: calc(100% - #{$header-height} - #{$performance-bar-height}); + height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height}); } } @@ -617,6 +617,8 @@ } .issuable-actions { + @include new-style-dropdown; + padding-top: 10px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8609f72bdab..439636fe026 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -645,7 +645,7 @@ } .merge-request-tabs-holder { - top: $header-height; + top: $new-navbar-height; z-index: 200; background-color: $white-light; border-bottom: 1px solid $border-color; @@ -675,7 +675,7 @@ } .with-performance-bar .merge-request-tabs-holder { - top: $header-height + $performance-bar-height; + top: $new-navbar-height + $performance-bar-height; } .merge-request-tabs { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 45f2aed1531..e437bad4912 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -516,7 +516,7 @@ ul.notes { } .note-actions-item { - margin-left: 15px; + margin-left: 12px; display: flex; align-items: center; @@ -620,15 +620,25 @@ ul.notes { .note-role { position: relative; - padding: 0 7px; + display: inline-block; color: $notes-role-color; font-size: 12px; line-height: 20px; - border: 1px solid $border-color; - border-radius: $label-border-radius; + margin: 0 3px; + + &.note-role-access { + padding: 0 7px; + border: 1px solid $border-color; + border-radius: $label-border-radius; + } + + &.note-role-special { + text-shadow: 0 0 15px $gl-text-color-inverted; + } } + /** * Line note button on the side of diffs */ diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 8d73246223d..615020ca856 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -190,6 +190,8 @@ input[type="checkbox"]:hover { } .search-holder { + @include new-style-dropdown; + @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb index bdc4332ae69..12a27cede75 100644 --- a/app/controllers/admin/logs_controller.rb +++ b/app/controllers/admin/logs_controller.rb @@ -1,6 +1,13 @@ class Admin::LogsController < Admin::ApplicationController + before_action :loggers + def show - @loggers = [ + end + + private + + def loggers + @loggers ||= [ Gitlab::AppLogger, Gitlab::GitLogger, Gitlab::EnvironmentLogger, diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a34a82b7ba6..23909bd2d39 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -36,6 +36,34 @@ module IssuableCollections @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) end + def redirect_out_of_range(relation, total_pages) + return false if total_pages.zero? + + out_of_range = relation.current_page > total_pages + + if out_of_range + redirect_to(url_for(params.merge(page: total_pages, only_path: true))) + end + + out_of_range + end + + def issues_page_count(relation) + page_count_for_relation(relation, issues_finder.row_count) + end + + def merge_requests_page_count(relation) + page_count_for_relation(relation, merge_requests_finder.row_count) + end + + def page_count_for_relation(relation, row_count) + limit = relation.limit_value.to_f + + return 1 if limit.zero? + + (row_count.to_f / limit).ceil + end + def issuable_finder_for(finder_class) finder_class.new(current_user, filter_params) end diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb new file mode 100644 index 00000000000..bb2c1dfa00a --- /dev/null +++ b/app/controllers/concerns/renders_commits.rb @@ -0,0 +1,7 @@ +module RendersCommits + def prepare_commits_for_rendering(commits) + Banzai::CommitRenderer.render(commits, @project, current_user) + + commits + end +end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index 41c3114ad1e..4791bc561a4 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -1,7 +1,8 @@ module RendersNotes - def prepare_notes_for_rendering(notes) + def prepare_notes_for_rendering(notes, noteable = nil) preload_noteable_for_regular_notes(notes) preload_max_access_for_authors(notes, @project) + preload_first_time_contribution_for_authors(noteable, notes) Banzai::NoteRenderer.render(notes, @project, current_user) notes @@ -19,4 +20,10 @@ module RendersNotes def preload_noteable_for_regular_notes(notes) ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable) end + + def preload_first_time_contribution_for_authors(noteable, notes) + return unless noteable.is_a?(Issuable) && noteable.first_contribution? + + notes.each {|n| n.specialize_for_first_contribution!(noteable)} + end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 076076fd1b3..d83824fef06 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -9,8 +9,6 @@ class ProfilesController < Profiles::ApplicationController end def update - user_params.except!(:email) if @user.external_email? - respond_to do |format| result = Users::UpdateService.new(@user, user_params).execute diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index f637a9a803b..eb010923466 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -7,7 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :authorize_update_build!, only: [:keep] before_action :extract_ref_name_and_path before_action :validate_artifacts! - before_action :set_path_and_entry, only: [:file, :raw] + before_action :entry, only: [:file] def download if artifacts_file.file_storage? @@ -41,7 +41,10 @@ class Projects::ArtifactsController < Projects::ApplicationController end def raw - send_artifacts_entry(build, @entry) + path = Gitlab::Ci::Build::Artifacts::Path + .new(params[:path]) + + send_artifacts_entry(build, path) end def keep @@ -93,9 +96,8 @@ class Projects::ArtifactsController < Projects::ApplicationController @artifacts_file ||= build.artifacts_file end - def set_path_and_entry - @path = params[:path] - @entry = build.artifacts_metadata_entry(@path) + def entry + @entry = build.artifacts_metadata_entry(params[:path]) render_404 unless @entry.exists? end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6de125e7e80..1a775def506 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -127,7 +127,7 @@ class Projects::CommitController < Projects::ApplicationController @discussions = commit.discussions @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) - @notes = prepare_notes_for_rendering(@notes) + @notes = prepare_notes_for_rendering(@notes, @commit) end def assign_change_commit_vars diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 2de9900d449..4a841bf2073 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -2,6 +2,7 @@ require "base64" class Projects::CommitsController < Projects::ApplicationController include ExtractsPath + include RendersCommits before_action :require_non_empty_project before_action :assign_ref_vars @@ -56,5 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController else @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end + + @commits = prepare_commits_for_rendering(@commits) end end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index c8613c0d634..193549663ac 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -3,6 +3,7 @@ require 'addressable/uri' class Projects::CompareController < Projects::ApplicationController include DiffForPath include DiffHelper + include RendersCommits # Authorize before_action :require_non_empty_project @@ -50,7 +51,7 @@ class Projects::CompareController < Projects::ApplicationController .execute(@project, @start_ref) if @compare - @commits = @compare.commits + @commits = prepare_commits_for_rendering(@compare.commits) @diffs = @compare.diffs(diff_options) environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0d4266f0899..ab9f132b502 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController @issues = issues_collection @issues = @issues.page(params[:page]) @issuable_meta_data = issuable_meta_data(@issues, @collection_type) + @total_pages = issues_page_count(@issues) - if @issues.out_of_range? && @issues.total_pages != 0 - return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true)) - end + return if redirect_out_of_range(@issues, @total_pages) if params[:label_name].present? @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @@ -86,7 +85,7 @@ class Projects::IssuesController < Projects::ApplicationController @note = @project.notes.new(noteable: @issue) @discussions = @issue.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable) respond_to do |format| format.html diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index f35d53896ba..1096afbb798 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -1,6 +1,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController include DiffForPath include DiffHelper + include RendersCommits skip_before_action :merge_request skip_before_action :ensure_ref_fetched @@ -107,7 +108,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @target_project = @merge_request.target_project @source_project = @merge_request.source_project - @commits = @merge_request.commits + @commits = prepare_commits_for_rendering(@merge_request.commits) @commit = @merge_request.diff_head_commit @note_counts = Note.where(commit_id: @commits.map(&:id)) diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 330b7df4541..109418c73f7 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -61,6 +61,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) - @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes)) + @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e3fa3736808..3aa5dadb5ca 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo include ToggleSubscriptionAction include IssuableActions include RendersNotes + include RendersCommits include ToggleAwardEmoji include IssuableCollections @@ -18,10 +19,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) + @total_pages = merge_requests_page_count(@merge_requests) - if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 - return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true)) - end + return if redirect_out_of_range(@merge_requests, @total_pages) if params[:label_name].present? labels_params = { project_id: @project.id, title: params[:label_name] } @@ -61,12 +61,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @discussions = @merge_request.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) - @noteable = @merge_request @commits_count = @merge_request.commits_count + @discussions = @merge_request.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable) + labels set_pipeline_variables @@ -95,7 +95,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo def commits # Get commits from repository # or from cache if already merged - @commits = @merge_request.commits + @commits = prepare_commits_for_rendering(@merge_request.commits) @note_counts = Note.where(commit_id: @commits.map(&:id)) .group(:commit_id).count diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index d07143d294f..7c19aa7bb23 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -64,7 +64,7 @@ class Projects::SnippetsController < Projects::ApplicationController @noteable = @snippet @discussions = @snippet.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable) render 'show' end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ed17b3b4689..b13034d3333 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -323,6 +323,7 @@ class ProjectsController < Projects::ApplicationController :build_allow_git_fetch, :build_coverage_regex, :build_timeout_in_minutes, + :resolve_outdated_diff_discussions, :container_registry_enabled, :default_branch, :description, diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index d58c8d14a75..fbad9ba7db8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -2,6 +2,7 @@ class SearchController < ApplicationController skip_before_action :authenticate_user! include SearchHelper + include RendersCommits layout 'search' @@ -20,6 +21,8 @@ class SearchController < ApplicationController @search_results = search_service.search_results @search_objects = search_service.search_objects + render_commits if @scope == 'commits' + check_single_commit_result end @@ -38,6 +41,10 @@ class SearchController < ApplicationController private + def render_commits + @search_objects = prepare_commits_for_rendering(@search_objects) + end + def check_single_commit_result if @search_results.single_commit_result? only_commit = @search_results.objects('commits').first diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 8c3abd0a085..c1cdc7c9831 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -66,7 +66,7 @@ class SnippetsController < ApplicationController @noteable = @snippet @discussions = @snippet.discussions - @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable) respond_to do |format| format.html do diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c8dd2275730..9848497f258 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -61,6 +61,10 @@ class IssuableFinder execute.find_by(*params) end + def row_count + Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state]) + end + # We often get counts for each state by running a query per state, and # counting those results. This is typically slower than running one query # (even if that query is slower than any of the individual state queries) and diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 017df8f6794..8d02d5de5c3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -302,10 +302,6 @@ module ApplicationHelper end end - def show_new_nav? - true - end - def collapsed_sidebar? cookies["sidebar_collapsed"] == "true" end diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb index d1dc4d94560..089d9e3e387 100644 --- a/app/helpers/blame_helper.rb +++ b/app/helpers/blame_helper.rb @@ -11,11 +11,15 @@ module BlameHelper end def age_map_class(commit_date, duration) - commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day - # Numbers 0 to 10 come from this calculation, but only commits on the oldest - # day get number 10 (all other numbers can be multiple days), so the range - # is normalized to 0-9 - age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min - "blame-commit-age-#{age_group}" + if duration[:started_days_ago] == 0 + "blame-commit-age-0" + else + commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day + # Numbers 0 to 10 come from this calculation, but only commits on the oldest + # day get number 10 (all other numbers can be multiple days), so the range + # is normalized to 0-9 + age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min + "blame-commit-age-#{age_group}" + end end end diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb index abe8edd6a8c..ee1b7ed083e 100644 --- a/app/helpers/breadcrumbs_helper.rb +++ b/app/helpers/breadcrumbs_helper.rb @@ -22,4 +22,16 @@ module BreadcrumbsHelper @breadcrumb_title = title end + + def breadcrumb_list_item(link) + content_tag "li" do + link + icon("angle-right", class: "breadcrumbs-list-angle") + end + end + + def add_to_breadcrumb_dropdown(link, location: :before) + @breadcrumb_dropdown_links ||= {} + @breadcrumb_dropdown_links[location] ||= [] + @breadcrumb_dropdown_links[location] << link + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index dd159d12aa0..eab1feb8a1f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -15,18 +15,20 @@ module GroupsHelper @has_group_title = true full_title = '' - group.ancestors.reverse.each do |parent| - full_title += group_title_link(parent, hidable: true) - - full_title += '<span class="hidable"> / </span>'.html_safe + group.ancestors.reverse.each_with_index do |parent, index| + if index > 0 + add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before) + else + full_title += breadcrumb_list_item group_title_link(parent, hidable: false) + end end - full_title += group_title_link(group) - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name + full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups") - content_tag :span, class: 'group-title' do - full_title.html_safe - end + full_title += breadcrumb_list_item group_title_link(group) + full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name + + full_title.html_safe end def projects_lfs_status(group) @@ -65,11 +67,11 @@ module GroupsHelper private - def group_title_link(group, hidable: false) - link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do + def group_title_link(group, hidable: false, show_avatar: false) + link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do output = - if show_new_nav? && !Rails.env.test? - image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) + if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? + image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) else "" end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 681615dbf3e..ce2999e6696 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -126,22 +126,20 @@ module IssuablesHelper end def issuable_meta(issuable, project, text) - output = content_tag(:strong, class: "identifier") do - concat("#{text} ") - concat(to_url_reference(issuable)) - end - - output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe + output = "" + output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") end output << " ".html_safe + output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) + 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 + output.html_safe end def issuable_todo(issuable) @@ -173,6 +171,13 @@ module IssuablesHelper html.html_safe end + def issuable_first_contribution_icon + content_tag(:span, class: 'fa-stack') do + concat(icon('certificate', class: "fa-stack-2x")) + concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x')) + end + end + def assigned_issuables_count(issuable_type) case issuable_type when :issues @@ -240,7 +245,8 @@ module IssuablesHelper def issuables_count_for_state(issuable_type, state) finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend - finder.count_by_state[state] + + Gitlab::IssuablesCountForState.new(finder)[state] end def close_issuable_url(issuable) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 941cfce8370..46bced00c72 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -21,25 +21,28 @@ module MarkupHelper end # Use this in places where you would normally use link_to(gfm(...), ...). - # + def link_to_markdown(body, url, html_options = {}) + return '' if body.blank? + + link_to_html(markdown(body, pipeline: :single_line), url, html_options) + end + + def link_to_markdown_field(object, field, url, html_options = {}) + rendered_field = markdown_field(object, field) + + link_to_html(rendered_field, url, html_options) + end + # It solves a problem occurring with nested links (i.e. # "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be # interpreted as intended. Browsers will parse something like # "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is - # not linked any more). link_to_gfm corrects that. It wraps all parts to + # not linked any more). link_to_html corrects that. It wraps all parts to # explicitly produce the correct linking behavior (i.e. # "<a>outer text </a><a>gfm ref</a><a> more outer text</a>"). - def link_to_gfm(body, url, html_options = {}) - return '' if body.blank? + def link_to_html(redacted, url, html_options = {}) + fragment = Nokogiri::HTML::DocumentFragment.parse(redacted) - context = { - project: @project, - current_user: (current_user if defined?(current_user)), - pipeline: :single_line - } - gfm_body = Banzai.render(body, context) - - fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) if fragment.children.size == 1 && fragment.children[0].name == 'a' # Fragment has only one node, and it's a link generated by `gfm`. # Replace it with our requested link. @@ -82,7 +85,10 @@ module MarkupHelper def markdown_field(object, field) object = object.for_display if object.respond_to?(:for_display) + redacted_field_html = object.try(:"redacted_#{field}_html") + return '' unless object.present? + return redacted_field_html if redacted_field_html html = Banzai.render_field(object, field) prepare_for_rendering(html, object.banzai_render_context(field)) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index b63b3b70903..a23a43c9f43 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,8 +1,8 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class - class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar - class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar + class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar class_name end @@ -30,24 +30,15 @@ module NavHelper end end - def nav_header_class - class_names = [] - class_names << 'with-horizontal-nav' if defined?(nav) && nav - - class_names + def nav_control_class + "nav-control" if current_user end - def layout_nav_class - return [] if show_new_nav? - + def user_dropdown_class class_names = [] - class_names << 'page-with-layout-nav' if defined?(nav) && nav - class_names << 'page-with-sub-nav' if content_for?(:sub_nav) + class_names << 'header-user-dropdown-toggle' + class_names << 'impersonated-user' if session[:impersonator_id] class_names end - - def nav_control_class - "nav-control" if current_user - end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 8c5e258f519..ce028195e51 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -73,7 +73,7 @@ module NotesHelper end def note_max_access_for_user(note) - note.project.team.human_max_access(note.author_id) + note.project.team.max_member_access(note.author_id) end def discussion_path(discussion) @@ -146,4 +146,8 @@ module NotesHelper autocomplete: autocomplete } end + + def discussion_resolved_intro(discussion) + discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved' + end end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index b30b2eb1d03..5946c475835 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -4,7 +4,7 @@ module PageLayoutHelper @page_title.push(*titles.compact) if titles.any? - if show_new_nav? && titles.any? && !defined?(@breadcrumb_title) + if titles.any? && !defined?(@breadcrumb_title) @breadcrumb_title = @page_title.last end @@ -80,7 +80,9 @@ module PageLayoutHelper @header_title = title @header_title_url = title_url else - @header_title_url ? link_to(@header_title, @header_title_url) : @header_title + return @header_title unless @header_title_url + + breadcrumb_list_item(link_to(@header_title, @header_title_url)) end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 45238f12ac7..5a4fda0724c 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,7 +1,12 @@ module ProfilesHelper - def email_provider_label - return unless current_user.external_email? - - current_user.email_provider.present? ? Gitlab::OAuth::Provider.label_for(current_user.email_provider) : "LDAP" + def attribute_provider_label(attribute) + user_synced_attributes_metadata = current_user.user_synced_attributes_metadata + if user_synced_attributes_metadata&.synced?(attribute) + if user_synced_attributes_metadata.provider + Gitlab::OAuth::Provider.label_for(user_synced_attributes_metadata.provider) + else + 'LDAP' + end + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 0bf94fd30db..86665ea2aec 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -54,31 +54,28 @@ module ProjectsHelper def project_title(project) namespace_link = if project.group - group_title(project.group) + group_title(project.group, nil, nil) else owner = project.namespace.owner link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to project_path(project), { class: "project-item-select-holder" } do + project_link = link_to project_path(project) do output = - if show_new_nav? && !Rails.env.test? - project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) + if project.avatar_url && !Rails.env.test? + project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) else "" end - output << simple_sanitize(project.name) + output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text") output.html_safe end - if current_user - project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do - icon("chevron-down") - end - end + namespace_link = breadcrumb_list_item(namespace_link) unless project.group + project_link = breadcrumb_list_item project_link - "#{namespace_link} / #{project_link}".html_safe + "#{namespace_link} #{project_link}".html_safe end def remove_project_message(project) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ae0e0aa3cf9..98e824a8c65 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -10,6 +10,7 @@ module SearchHelper search_pattern = Regexp.new(Regexp.escape(term), "i") generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.concat(default_autocomplete_admin) if current_user.admin? generic_results.select! { |result| result[:label] =~ search_pattern } [ @@ -41,8 +42,14 @@ module SearchHelper [ { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, - { category: "Settings", label: "Dashboard", url: root_path }, - { category: "Settings", label: "Admin Section", url: admin_root_path } + { category: "Settings", label: "Dashboard", url: root_path } + ] + end + + # Autocomplete results for settings pages, for admins + def default_autocomplete_admin + [ + { category: "Settings", label: "Admin Section", url: admin_root_path } ] end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index e0d3e9b88f3..95dbdc8ec46 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -99,7 +99,9 @@ module TreeHelper end # returns the relative path of the first subdir that doesn't have only one directory descendant - def flatten_tree(tree) + def flatten_tree(root_path, tree) + return tree.flat_path.sub(/\A#{root_path}\//, '') if tree.flat_path.present? + subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? return tree_join(tree.name, flatten_tree(subtree.first)) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 99212a3438f..815fab9e061 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -10,4 +10,15 @@ module WikiHelper .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } .join(' / ') end + + def wiki_breadcrumb_dropdown_links(page_slug) + page_slug_split = page_slug.split('/') + page_slug_split.pop(1) + current_slug = "" + page_slug_split + .map do |dir_or_page| + current_slug = "#{current_slug}#{dir_or_page}/" + add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after + end + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ba3156154ac..64c93966dff 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,7 +27,6 @@ module Ci validates :coverage, numericality: true, allow_blank: true validates :ref, presence: true - validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } @@ -451,6 +450,10 @@ module Ci trace end + def serializable_hash(options = {}) + super(options).merge(when: read_attribute(:when)) + end + private def update_artifacts_size diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 35d14b6e297..46e5c344fdc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -36,7 +36,6 @@ module Ci validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } - validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create validate :valid_commit_sha, unless: :importing? after_create :keep_around_commits, unless: :importing? diff --git a/app/models/commit.rb b/app/models/commit.rb index c943365016f..2ae8890c1b3 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -16,6 +16,8 @@ class Commit participant :notes_with_associations attr_accessor :project, :author + attr_accessor :redacted_description_html + attr_accessor :redacted_title_html DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] @@ -26,6 +28,13 @@ class Commit # The SHA can be between 7 and 40 hex characters. COMMIT_SHA_PATTERN = '\h{7,40}'.freeze + def banzai_render_context(field) + context = { pipeline: :single_line, project: self.project } + context[:author] = self.author if self.author + + context + end + class << self def decorate(commits, project) commits.map do |commit| @@ -405,6 +414,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) + @gpg_commit ||= Gitlab::Gpg::Commit.new(self) end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3731b7c8577..265f6e48540 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,6 +6,7 @@ # module Issuable extend ActiveSupport::Concern + include Gitlab::SQL::Pattern include CacheMarkdownField include Participable include Mentionable @@ -122,7 +123,9 @@ module Issuable # # Returns an ActiveRecord::Relation. def search(query) - where(arel_table[:title].matches("%#{query}%")) + title = to_fuzzy_arel(:title, query) + + where(title) end # Searches for records with a matching title or description. @@ -133,10 +136,10 @@ module Issuable # # Returns an ActiveRecord::Relation. def full_search(query) - t = arel_table - pattern = "%#{query}%" + title = to_fuzzy_arel(:title, query) + description = to_fuzzy_arel(:description, query) - where(t[:title].matches(pattern).or(t[:description].matches(pattern))) + where(title&.or(description)) end def sort(method, excluded_labels: []) @@ -331,4 +334,11 @@ module Issuable metrics = self.metrics || create_metrics metrics.record! end + + ## + # Override in issuable specialization + # + def first_contribution? + false + end end diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index dd979e7bb17..f006a271327 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -24,6 +24,7 @@ module ResolvableDiscussion delegate :resolved_at, :resolved_by, + :resolved_by_push?, to: :last_resolved_note, allow_nil: true diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 05eb6f86704..668c5a079e3 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -51,22 +51,34 @@ module ResolvableNote end # If you update this method remember to also update `.resolve!` - def resolve!(current_user) - return unless resolvable? - return if resolved? + def resolve_without_save(current_user, resolved_by_push: false) + return false unless resolvable? + return false if resolved? self.resolved_at = Time.now self.resolved_by = current_user - save! + self.resolved_by_push = resolved_by_push + + true end # If you update this method remember to also update `.unresolve!` - def unresolve! - return unless resolvable? - return unless resolved? + def unresolve_without_save + return false unless resolvable? + return false unless resolved? self.resolved_at = nil self.resolved_by = nil - save! + + true + end + + def resolve!(current_user, resolved_by_push: false) + resolve_without_save(current_user, resolved_by_push: resolved_by_push) && + save! + end + + def unresolve! + unresolve_without_save && save! end end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 3df60ddc950..1633acd4fa9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base def verified_user_infos user_infos.select do |user_info| - user_info[:email] == user.email + user.verified_email?(user_info[:email]) end end @@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base user_infos.map do |user_info| [ user_info[:email], - user_info[:email] == user.email + user.verified_email?(user_info[:email]) ] end.to_h end def verified? - emails_with_verified_status.any? { |_email, verified| verified } + emails_with_verified_status.values.any? + end + + def verified_and_belongs_to_email?(email) + emails_with_verified_status.fetch(email, false) end def update_invalid_gpg_signatures @@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base end def revoke - GpgSignature.where(gpg_key: self, valid_signature: true).update_all( - gpg_key_id: nil, - valid_signature: false, - updated_at: Time.zone.now - ) + GpgSignature + .where(gpg_key: self) + .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) + .update_all( + gpg_key_id: nil, + verification_status: GpgSignature.verification_statuses[:unknown_key], + updated_at: Time.zone.now + ) destroy end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 50fb35c77ec..454c90d5fc4 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,9 +1,21 @@ class GpgSignature < ActiveRecord::Base include ShaAttribute + include IgnorableColumn + + ignore_column :valid_signature sha_attribute :commit_sha sha_attribute :gpg_key_primary_keyid + enum verification_status: { + unverified: 0, + verified: 1, + same_user_different_email: 2, + other_user: 3, + unverified_key: 4, + unknown_key: 5 + } + belongs_to :project belongs_to :gpg_key @@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit - Gitlab::Gpg::Commit.new(project, commit_sha) + Gitlab::Gpg::Commit.new(commit) end end diff --git a/app/models/group.rb b/app/models/group.rb index 190b27cf66b..e746e4a12c9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -16,6 +16,7 @@ class Group < Namespace source: :user has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'GroupMember' has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/member.rb b/app/models/member.rb index ee2cb13697b..cbbd58f2eaf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -126,20 +126,11 @@ class Member < ActiveRecord::Base find_by(invite_token: invite_token) end - def add_user(source, user, access_level, current_user: nil, expires_at: nil) - user = retrieve_user(user) + def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil) + # `user` can be either a User object, User ID or an email to be invited + member = retrieve_member(source, user, existing_members) access_level = retrieve_access_level(access_level) - # `user` can be either a User object or an email to be invited - member = - if user.is_a?(User) - source.members.find_by(user_id: user.id) || - source.requesters.find_by(user_id: user.id) || - source.members.build(user_id: user.id) - else - source.members.build(invite_email: user) - end - return member unless can_update_member?(current_user, member) member.attributes = { @@ -165,17 +156,15 @@ class Member < ActiveRecord::Base def add_users(source, users, access_level, current_user: nil, expires_at: nil) return [] unless users.present? - # Collect all user ids into separate array - # so we can use single sql query to get user objects - user_ids = users.select { |user| user =~ /\A\d+\Z/ } - users = users - user_ids + User.where(id: user_ids) + emails, users, existing_members = parse_users_list(source, users) self.transaction do - users.map do |user| + (emails + users).map! do |user| add_user( source, user, access_level, + existing_members: existing_members, current_user: current_user, expires_at: expires_at ) @@ -189,6 +178,31 @@ class Member < ActiveRecord::Base private + def parse_users_list(source, list) + emails, user_ids, users = [], [], [] + existing_members = {} + + list.each do |item| + case item + when User + users << item + when Integer + user_ids << item + when /\A\d+\Z/ + user_ids << item.to_i + when Devise.email_regexp + emails << item + end + end + + if user_ids.present? + users.concat(User.where(id: user_ids)) + existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id) + end + + [emails, users, existing_members] + end + # This method is used to find users that have been entered into the "Add members" field. # These can be the User objects directly, their IDs, their emails, or new emails to be invited. def retrieve_user(user) @@ -197,6 +211,20 @@ class Member < ActiveRecord::Base User.find_by(id: user) || User.find_by(email: user) || user end + def retrieve_member(source, user, existing_members) + user = retrieve_user(user) + + if user.is_a?(User) + if existing_members + existing_members[user.id] || source.members.build(user_id: user.id) + else + source.members_and_requesters.find_or_initialize_by(user_id: user.id) + end + else + source.members.build(invite_email: user) + end + end + def retrieve_access_level(access_level) access_levels.fetch(access_level) { access_level.to_i } end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 724fb4ccef1..2a56bab48a3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -918,6 +918,12 @@ class MergeRequest < ActiveRecord::Base active_diff_discussions.each do |discussion| service.execute(discussion) end + + if project.resolve_outdated_diff_discussions? + MergeRequests::ResolvedDiscussionNotificationService + .new(project, current_user) + .execute(self) + end end def keep_around_commit @@ -954,6 +960,12 @@ class MergeRequest < ActiveRecord::Base Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache end + def first_contribution? + return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST + + project.merge_requests.merged.where(author_id: author_id).empty? + end + private def write_ref diff --git a/app/models/note.rb b/app/models/note.rb index 1073c115630..f44590e2144 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -15,6 +15,16 @@ class Note < ActiveRecord::Base include IgnorableColumn include Editable + module SpecialRole + FIRST_TIME_CONTRIBUTOR = :first_time_contributor + + class << self + def values + constants.map {|const| self.const_get(const)} + end + end + end + ignore_column :original_discussion_id cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true @@ -32,9 +42,12 @@ class Note < ActiveRecord::Base # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count - # Attribute used to store the attributes that have ben changed by quick actions. + # Attribute used to store the attributes that have been changed by quick actions. attr_accessor :commands_changes + # A special role that may be displayed on issuable's discussions + attr_accessor :special_role + default_value_for :system, false attr_mentionable :note, pipeline: :note @@ -141,6 +154,10 @@ class Note < ActiveRecord::Base .group(:noteable_id) .where(noteable_type: type, noteable_id: ids) end + + def has_special_role?(role, note) + note.special_role == role + end end def cross_reference? @@ -206,6 +223,22 @@ class Note < ActiveRecord::Base super(noteable_type.to_s.classify.constantize.base_class.to_s) end + def special_role=(role) + raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.values.include?(role) + + @special_role = role + end + + def has_special_role?(role) + self.class.has_special_role?(role, self) + end + + def specialize_for_first_contribution!(noteable) + return unless noteable.author_id == self.author_id + + self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR + end + def editable? !system? end diff --git a/app/models/project.rb b/app/models/project.rb index 051c4c8e2ec..fdd516ec2ae 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,7 @@ class Project < ActiveRecord::Base default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level + default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } @@ -144,6 +145,7 @@ class Project < ActiveRecord::Base has_many :requesters, -> { where.not(requested_at: nil) }, as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :members_and_requesters, as: :source, class_name: 'ProjectMember' has_many :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 674eacd28e8..09049824ff7 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -150,7 +150,7 @@ class ProjectTeam end def human_max_access(user_id) - Gitlab::Access.options_with_owner.key(max_member_access(user_id)) + Gitlab::Access.human_access(max_member_access(user_id)) end # Determine the maximum access level for a group of users in bulk. diff --git a/app/models/repository.rb b/app/models/repository.rb index 05f2f851162..035f85a0b46 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -166,32 +166,25 @@ class Repository end def add_branch(user, branch_name, ref) - newrev = commit(ref).try(:sha) - - return false unless newrev - - Gitlab::Git::OperationService.new(user, raw_repository).add_branch(branch_name, newrev) + branch = raw_repository.add_branch(branch_name, committer: user, target: ref) after_create_branch - find_branch(branch_name) + + branch + rescue Gitlab::Git::Repository::InvalidRef + false end def add_tag(user, tag_name, target, message = nil) - newrev = commit(target).try(:id) - options = { message: message, tagger: user_to_committer(user) } if message - - return false unless newrev - - Gitlab::Git::OperationService.new(user, raw_repository).add_tag(tag_name, newrev, options) - - find_tag(tag_name) + raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + rescue Gitlab::Git::Repository::InvalidRef + false end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - Gitlab::Git::OperationService.new(user, raw_repository).rm_branch(branch) + raw_repository.rm_branch(branch_name, committer: user) after_remove_branch true @@ -199,9 +192,8 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - tag = find_tag(tag_name) - Gitlab::Git::OperationService.new(user, raw_repository).rm_tag(tag) + raw_repository.rm_tag(tag_name, committer: user) after_remove_tag true diff --git a/app/models/user.rb b/app/models/user.rb index 9d48c82e861..105eb62f1fa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,10 +15,12 @@ class User < ActiveRecord::Base include IgnorableColumn include FeatureGate include CreatedAtFilterable + include IgnorableColumn DEFAULT_NOTIFICATION_LEVEL = :participating - ignore_column :authorized_projects_populated + ignore_column :external_email + ignore_column :email_provider add_authentication_token_field :authentication_token add_authentication_token_field :incoming_email_token @@ -85,6 +87,7 @@ class User < ActiveRecord::Base has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_one :user_synced_attributes_metadata, autosave: true # Groups has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -161,6 +164,7 @@ class User < ActiveRecord::Base after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? + before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } after_save :ensure_namespace_correct after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } after_initialize :set_projects_limit @@ -1041,6 +1045,26 @@ class User < ActiveRecord::Base ensure_rss_token! end + def verified_email?(email) + self.email == email + end + + def sync_attribute?(attribute) + return true if ldap_user? && attribute == :email + + attributes = Gitlab.config.omniauth.sync_profile_attributes + + if attributes.is_a?(Array) + attributes.include?(attribute.to_s) + else + attributes + end + end + + def read_only_attribute?(attribute) + user_synced_attributes_metadata&.read_only?(attribute) + end + protected # override, from Devise::Validatable diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb new file mode 100644 index 00000000000..9f374304164 --- /dev/null +++ b/app/models/user_synced_attributes_metadata.rb @@ -0,0 +1,25 @@ +class UserSyncedAttributesMetadata < ActiveRecord::Base + belongs_to :user + + validates :user, presence: true + + SYNCABLE_ATTRIBUTES = %i[name email location].freeze + + def read_only?(attribute) + Gitlab.config.omniauth.sync_profile_from_provider && synced?(attribute) + end + + def read_only_attributes + return [] unless Gitlab.config.omniauth.sync_profile_from_provider + + SYNCABLE_ATTRIBUTES.select { |key| synced?(key) } + end + + def synced?(attribute) + read_attribute("#{attribute}_synced") + end + + def set_attribute_synced(attribute, value) + write_attribute("#{attribute}_synced", value) + end +end diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb index 1ef8d9edbe1..746f209e20f 100644 --- a/app/services/discussions/update_diff_position_service.rb +++ b/app/services/discussions/update_diff_position_service.rb @@ -10,6 +10,10 @@ module Discussions discussion.notes.each do |note| if outdated note.change_position = position + + if project.resolve_outdated_diff_discussions? + note.resolve_without_save(current_user, resolved_by_push: true) + end else note.position = position note.change_position = nil diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index 2f9855273dc..6188b8a4349 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -34,6 +34,10 @@ module Users private def assign_attributes(&block) + if @user.user_synced_attributes_metadata + params.except!(*@user.user_synced_attributes_metadata.read_only_attributes) + end + @user.assign_attributes(params) if params.any? end end diff --git a/app/views/admin/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml index 13b583e6072..13c408914bb 100644 --- a/app/views/admin/applications/edit.html.haml +++ b/app/views/admin/applications/edit.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Applications", admin_applications_path +- breadcrumb_title @application.name - page_title "Edit", @application.name, "Applications" %h3.page-title Edit application diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml index be8644c0ca6..bff53da1d9a 100644 --- a/app/views/admin/cohorts/index.html.haml +++ b/app/views/admin/cohorts/index.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Cohorts" - @no_container = true = render "admin/dashboard/head" diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8e94e68bc11..069f8f89e0b 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,4 +1,5 @@ - @no_container = true +- breadcrumb_title "Dashboard" = render "admin/dashboard/head" %div{ class: container_class } diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 2aadc071c75..3e02f7b1e16 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Groups", admin_groups_path +- breadcrumb_title @group.name - page_title @group.name, "Groups" %h3.page-title Group: #{@group.full_name} diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml index 665e8c7e74f..efb15ccc8df 100644 --- a/app/views/admin/hooks/edit.html.haml +++ b/app/views/admin/hooks/edit.html.haml @@ -1,3 +1,4 @@ +- add_to_breadcrumbs "System Hooks", admin_hooks_path - page_title 'Edit System Hook' %h3.page-title Edit System Hook diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index 09be17f07be..aa6e9db3900 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Jobs" - @no_container = true = render "admin/dashboard/head" diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml index 309aedceded..96f0d404ac4 100644 --- a/app/views/admin/labels/edit.html.haml +++ b/app/views/admin/labels/edit.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Labels", admin_labels_path +- breadcrumb_title "Edit Label" - page_title "Edit", @label.name, "Labels" %h3.page-title Edit Label diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 7b1b15cfeb8..ab4165c0bf2 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Projects", admin_projects_path +- breadcrumb_title @project.name_with_namespace - page_title @project.name_with_namespace, "Projects" %h3.page-title Project: #{@project.name_with_namespace} diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 126550ee10e..6793ce557c4 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Runners" - @no_container = true = render "admin/dashboard/head" diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml index 53d970e33c1..512176649e6 100644 --- a/app/views/admin/services/edit.html.haml +++ b/app/views/admin/services/edit.html.haml @@ -1,2 +1,4 @@ +- add_to_breadcrumbs "Service Templates", admin_application_settings_services_path +- breadcrumb_title @service.title - page_title @service.title, "Service Templates" = render 'form' diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index b556ff056c0..98ff592eb64 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Users", admin_users_path +- breadcrumb_title @user.name - page_title @user.name, "Users" = render 'admin/users/head' diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml index 98f618ca3b8..fbfe3e56588 100644 --- a/app/views/ci/variables/_content.html.haml +++ b/app/views/ci/variables/_content.html.haml @@ -1,9 +1,3 @@ -%h4.prepend-top-0 - Secret variables - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' -%p - These variables will be set to environment by the runner, and could be protected by exposing only to protected branches or tags. -%p - So you can use them for passwords, secret keys or whatever you want. -%p - The value of the variable can be visible in job log if explicitly asked to do so. +%p.append-bottom-default + Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. + You can use variables for passwords, secret keys, or whatever you want. diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index 007c2344b5a..2bac69bc536 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -1,7 +1,5 @@ .row.prepend-top-default.append-bottom-default - .col-lg-4 - = render "ci/variables/content" - .col-lg-8 + .col-lg-12 %h5.prepend-top-0 Add a variable = render "ci/variables/form", btn_text: "Add new variable" diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml index 2bfb290629d..6d75ae96124 100644 --- a/app/views/ci/variables/_show.html.haml +++ b/app/views/ci/variables/_show.html.haml @@ -4,6 +4,6 @@ .col-lg-3 = render "ci/variables/content" .col-lg-9 - %h5.prepend-top-0 + %h4.prepend-top-0 Update variable = render "ci/variables/form", btn_text: "Save variable" diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 5a379eae8f4..11bf3f5d323 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,4 +1,4 @@ -- if show_new_nav? && current_user.can_create_group? +- if current_user.can_create_group? - content_for :breadcrumbs_extra do = link_to "New group", new_group_path, class: "btn btn-new" @@ -10,8 +10,8 @@ = nav_link(page: explore_groups_path) do = link_to explore_groups_path, title: 'Explore public groups' do Explore public groups - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/groups/search_form' = render 'shared/groups/dropdown' - if current_user.can_create_group? - = link_to "New group", new_group_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" + = link_to "New group", new_group_path, class: "btn btn-new visible-xs" diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 1f9a5b401b6..e2a1914ada2 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,7 @@ = content_for :flash_message do = render 'shared/project_limit' -- if show_new_nav? && current_user.can_create_project? +- if current_user.can_create_project? - content_for :breadcrumbs_extra do = link_to "New project", new_project_path, class: 'btn btn-new' @@ -19,8 +19,8 @@ = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do Explore projects - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? - = link_to "New project", new_project_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}" + = link_to "New project", new_project_path, class: "btn btn-new visible-xs" diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index fd5389106bb..14c18678ab1 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,4 +1,4 @@ -- if show_new_nav? && current_user +- if current_user - content_for :breadcrumbs_extra do = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" @@ -10,7 +10,3 @@ = nav_link(page: explore_snippets_path) do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do Explore Snippets - - - if current_user - .nav-controls.hidden-xs{ class: ("hidden-sm hidden-md hidden-lg" if show_new_nav?) } - = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 9ac44674b73..ad0e205a79f 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,15 +4,14 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues") -- if show_new_nav? - - content_for :breadcrumbs_extra do - = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do - = icon('rss') - = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues +- content_for :breadcrumbs_extra do + = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do + = icon('rss') + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 960e1e55f36..ccc74f7cf3d 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,13 +2,12 @@ - page_title "Merge Requests" - header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id) -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests +- content_for :breadcrumbs_extra do + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index cb8bf57cba1..9fffdded1a0 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -2,14 +2,13 @@ - page_title 'Milestones' - header_title 'Milestones', dashboard_milestones_path -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones +- content_for :breadcrumbs_extra do + = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones .milestones diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml index 25e90924413..b865eb815f0 100644 --- a/app/views/discussions/_headline.html.haml +++ b/app/views/discussions/_headline.html.haml @@ -1,9 +1,11 @@ - if discussion.resolved? .discussion-headline-light.js-discussion-headline - Resolved + = discussion_resolved_intro(discussion) - if discussion.resolved_by by = link_to_member(@project, discussion.resolved_by, avatar: false) + - if discussion.resolved_by_push? + with a push = time_ago_with_tooltip(discussion.resolved_at, placement: "bottom") - elsif discussion.updated? .discussion-headline-light.js-discussion-headline diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 9ebb3894c55..839f23e69fd 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "General Settings" = render "groups/settings_head" .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 837ef385dd5..13a4b4c90c9 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,7 +8,7 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? && group_issues_exists +- if group_issues_exists - content_for :breadcrumbs_extra do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do = icon('rss') @@ -19,7 +19,7 @@ - if group_issues_exists .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to params.merge(rss_url_options), class: 'btn' do = icon('rss') %span.icon-label diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 50179a47797..9e59a09d459 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,5 +1,5 @@ - page_title 'Labels' -- if show_new_nav? && can?(current_user, :admin_label, @group) +- if can?(current_user, :admin_label, @group) - content_for :breadcrumbs_extra do = link_to "New label", new_group_label_path(@group), class: "btn btn-new" @@ -10,7 +10,7 @@ .nav-text Labels can be applied to issues and merge requests. Group labels are available for any project within the group. - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :admin_label, @group) = link_to "New label", new_group_label_path(@group), class: "btn btn-new" diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 184df6f5406..0344770e0dd 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -4,7 +4,7 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? && current_user +- if current_user - content_for :breadcrumbs_extra do = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests @@ -14,7 +14,7 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests - if current_user - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 66c6cc9e279..6e7a1af243d 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,5 +1,5 @@ - page_title "Milestones" -- if show_new_nav? && can?(current_user, :admin_milestones, @group) +- if can?(current_user, :admin_milestones, @group) - content_for :breadcrumbs_extra do = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" @@ -8,7 +8,7 @@ .top-area = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :admin_milestones, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 7a2e688a114..7f3f2f707f7 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Projects" = render "groups/settings_head" .panel.panel-default.prepend-top-default diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index bf36baf48ab..9f9ae01e7c5 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -1,4 +1,5 @@ -- page_title "Pipelines" +- breadcrumb_title "CI / CD Settings" +- page_title "CI / CD" = render "groups/settings_head" = render 'ci/variables/index' diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index e07f61c94e4..f4f76887422 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- breadcrumb_title "Group" +- breadcrumb_title "Details" = content_for :meta_tags do = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml index 8f0724c0677..7abc84412c6 100644 --- a/app/views/groups/subgroups.html.haml +++ b/app/views/groups/subgroups.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Details" - @no_container = true = render 'head' diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3babdae3968..34e85fef6d9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,9 +32,9 @@ = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? - - if show_new_nav? - = stylesheet_link_tag "new_nav", media: "all" - = stylesheet_link_tag "new_sidebar", media: "all" + // TODO: Combine these 2 stylesheets into application.scss + = stylesheet_link_tag "new_nav", media: "all" + = stylesheet_link_tag "new_sidebar", media: "all" = Gon::Base.render_data diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c4f8cd71395..1fd301d6850 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,26 +1,14 @@ .page-with-sidebar{ class: page_with_sidebar_class } - - if show_new_nav? - - if defined?(nav) && nav - = render "layouts/nav/#{nav}" - - else - - if defined?(nav) && nav - .layout-nav - .container-fluid - = render "layouts/nav/#{nav}" - - if content_for?(:sub_nav) - = yield :sub_nav - .content-wrapper{ class: layout_nav_class } - - if show_new_nav? - .mobile-overlay + - if defined?(nav) && nav + = render "layouts/nav/sidebar/#{nav}" + .content-wrapper.page-with-new-nav + .mobile-overlay .alert-wrapper = render "layouts/broadcast" - - if show_new_nav? - - if content_for?(:new_global_flash) - = yield :new_global_flash - - unless @hide_breadcrumbs - = render "layouts/nav/breadcrumbs" - = render "layouts/flash" = yield :flash_message + - unless @hide_breadcrumbs + = render "layouts/nav/breadcrumbs" + = render "layouts/flash" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } .content{ id: "content-body" } = yield diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 59f16b47bf7..cd7a47da4a1 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -17,8 +17,8 @@ .dropdown-menu.dropdown-select = dropdown_content do %ul - %li - %a.is-focused.dropdown-menu-empty-link + %li.dropdown-menu-empty-item + %a Loading... = dropdown_loading %i.search-icon diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index ae9eee215e0..8595157a997 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,9 +1,6 @@ - page_title "Admin Area" - header_title "Admin Area", admin_root_path -- if show_new_nav? - - nav "new_admin_sidebar" - - @new_sidebar = true -- else - - nav "admin" +- nav "admin" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b53f382fa3d..65ac8aaa59b 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,10 +4,7 @@ %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' - - if show_new_nav? - = render "layouts/header/new" - - else - = render "layouts/header/default", title: header_title + = render "layouts/header/default" = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 35abfa0e80c..08bd6fc311e 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,10 +1,7 @@ - page_title @group.name - page_description @group.description unless page_description - header_title group_title(@group) unless header_title -- if show_new_nav? - - nav "new_group_sidebar" - - @new_sidebar = true -- else - - nav "group" +- nav "group" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1d875f81041..d8fc371497d 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,68 +1,50 @@ -%header.navbar.navbar-gitlab{ class: nav_header_class } - .navbar-border +%header.navbar.navbar-gitlab.navbar-gitlab-new %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content - .dropdown.global-dropdown - %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.sr-only Toggle navigation - = icon('bars') - .dropdown-menu-nav.global-dropdown-menu - - if current_user - = render 'layouts/nav/dashboard' - - else - = render 'layouts/nav/explore' + .title-container + %h1.title + = link_to root_path, title: 'Dashboard', id: 'logo' do + = brand_header_logo + %span.logo-text.hidden-xs + = render 'shared/logo_type.svg' - .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do - = brand_header_logo - - .title-container.js-title-container - %h1.title{ class: ('initializing' if @has_group_title) }= title + - if current_user + = render "layouts/nav/dashboard" + - else + = render "layouts/nav/explore" .navbar-collapse.collapse %ul.nav.navbar-nav + - if current_user + = render 'layouts/header/new_dropdown' %li.hidden-sm.hidden-xs = render 'layouts/search' unless current_controller?(:search) %li.visible-sm-inline-block.visible-xs-inline-block = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('search') - if current_user - - if session[:impersonator_id] - %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do - = icon('user-secret fw') - - if current_user.admin? - %li - = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('wrench fw') - = render 'layouts/header/new_dropdown' - - if Gitlab::Sherlock.enabled? - %li - = link_to sherlock_transactions_path, title: 'Sherlock Transactions', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('tachometer fw') - %li - = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + %li.user-counter + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = custom_icon('issues') - issues_count = assigned_issuables_count(:issues) %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - %li - = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + %li.user-counter + = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = custom_icon('mr_bold') - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) - %li + %li.user-counter = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('check-circle fw') + = custom_icon('todo_done') %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) %li.header-user.dropdown - = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - = icon('caret-down') + = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do + = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" + = custom_icon('caret_down') .dropdown-menu-nav.dropdown-menu-align-right %ul %li.current-user @@ -74,18 +56,24 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path + - if current_user + %li + = link_to "Help", help_path %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" + - if session[:impersonator_id] + %li.impersonation + = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = icon('user-secret') - else %li %div - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' + = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' - %button.navbar-toggle{ type: 'button' } + %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %span.sr-only Toggle navigation - = icon('ellipsis-v') - - = yield :header_content + = icon('ellipsis-v', class: 'js-navbar-toggle-right') + = icon('times', class: 'js-navbar-toggle-left') = render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml deleted file mode 100644 index c84d7053cd6..00000000000 --- a/app/views/layouts/header/_new.html.haml +++ /dev/null @@ -1,84 +0,0 @@ -%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class } - %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content - .container-fluid - .header-content - .title-container - %h1.title - = link_to root_path, title: 'Dashboard', id: 'logo' do - = brand_header_logo - %span.logo-text.hidden-xs - = render 'shared/logo_type.svg' - - - if current_user - = render "layouts/nav/new_dashboard" - - else - = render "layouts/nav/new_explore" - - .navbar-collapse.collapse - %ul.nav.navbar-nav - %li.hidden-sm.hidden-xs - = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm-inline-block.visible-xs-inline-block - = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('search') - - if current_user - - if session[:impersonator_id] - %li.impersonation - = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do - = icon('user-secret fw') - - if current_user.admin? - %li - = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('wrench fw') - = render 'layouts/header/new_dropdown' - - if Gitlab::Sherlock.enabled? - %li - = link_to sherlock_transactions_path, title: 'Sherlock Transactions', - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('tachometer fw') - %li - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('issues') - - issues_count = assigned_issuables_count(:issues) - %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } - = number_with_delimiter(issues_count) - %li - = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = custom_icon('mr_bold') - - merge_requests_count = assigned_issuables_count(:merge_requests) - %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } - = number_with_delimiter(merge_requests_count) - %li - = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('check-circle fw') - %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } - = todos_count_format(todos_pending_count) - %li.header-user.dropdown - = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" - = icon('chevron-down') - .dropdown-menu-nav.dropdown-menu-align-right - %ul - %li.current-user - .user-name.bold - = current_user.name - @#{current_user.username} - %li.divider - %li - = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } - %li - = link_to "Settings", profile_path - %li.divider - %li - = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" - - else - %li - %div - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - - %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } - %span.sr-only Toggle navigation - = icon('ellipsis-v', class: 'js-navbar-toggle-right') - = icon('times', class: 'js-navbar-toggle-left') - -= render 'shared/outdated_browser' diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9da739b0974..63d1c077ecd 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,11 +1,7 @@ %li.header-new.dropdown = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do - - if show_new_nav? - = icon('plus') - = icon('chevron-down') - - else - = icon('plus fw') - = icon('caret-down') + = custom_icon('plus_square') + = custom_icon('caret_down') .dropdown-menu-nav.dropdown-menu-align-right %ul - if @group&.persisted? diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml deleted file mode 100644 index 6df0adfd742..00000000000 --- a/app/views/layouts/nav/_admin.html.haml +++ /dev/null @@ -1,40 +0,0 @@ -= render 'layouts/nav/admin_settings' -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - %span - Overview - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - %span - Monitoring - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - %span - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - %span - System Hooks - - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - %span - Applications - - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - %span - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do - %span - Spam Logs diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml deleted file mode 100644 index 9de0e12a826..00000000000 --- a/app/views/layouts/nav/_admin_settings.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -.controls - .dropdown.admin-settings-dropdown - %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - %span - Deploy Keys - - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - %span - Service Templates - - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - %span - Labels - - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - %span - Appearance - - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - %span - Settings diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index 653452871a0..feffd7707dc 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -1,28 +1,22 @@ -- breadcrumb_link = breadcrumb_title_link - container = @no_breadcrumb_container ? 'container-fluid' : container_class - hide_top_links = @hide_top_links || false -%nav.breadcrumbs{ role: "navigation" } +%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } .breadcrumbs-container{ class: [container, @content_class] } - - if defined?(@new_sidebar) + - if defined?(@left_sidebar) = button_tag class: 'toggle-mobile-nav', type: 'button' do %span.sr-only Open sidebar = icon ('bars') .breadcrumbs-links.js-title-container - - unless hide_top_links - .title - = link_to "GitLab", root_path - \/ - - if content_for?(:header_title_before) - = yield :header_title_before - \/ + %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list + - unless hide_top_links = header_title - %h2.breadcrumbs-sub-title - %ul.list-unstyled - - if @breadcrumbs_extra_links - - @breadcrumbs_extra_links.each do |extra| - %li= link_to extra[:text], extra[:link] - %li= link_to @breadcrumb_title, breadcrumb_link + - if @breadcrumbs_extra_links + - @breadcrumbs_extra_links.each do |extra| + = breadcrumb_list_item link_to(extra[:text], extra[:link]) + = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after + %li + %h2.breadcrumbs-sub-title= @breadcrumb_title - if content_for?(:breadcrumbs_extra) .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra = yield :header_content diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index be7d27df2a0..8a39c4d775f 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,67 +1,62 @@ -%ul - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - P - %span - Projects - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - A - %span - Activity - - if koding_enabled? - = nav_link(controller: :koding) do - = link_to koding_path, title: 'Koding' do - %span - Koding - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do +%ul.list-unstyled.navbar-sub-nav + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do + %a{ href: "#", data: { toggle: "dropdown" } } + Projects + = custom_icon('caret_down') + .dropdown-menu.projects-dropdown-menu + = render "layouts/nav/projects_dropdown/show" + + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - G - %span - Groups - = nav_link(controller: 'dashboard/milestones') do + Groups + + = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + Activity + + = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - L - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - I - %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues)) - %span - Issues - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - M - %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests)) - %span - Merge Requests - = nav_link(controller: 'dashboard/snippets') do + Milestones + + = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - S - %span - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab' + Snippets + + %li.dropdown.hidden-lg + %a{ href: "#", data: { toggle: "dropdown" } } + More + = custom_icon('caret_down') + .dropdown-menu + %ul + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + Groups + + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, title: 'Activity' do + Activity + + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do + Milestones + + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do + Snippets + + -# Shortcut to Dashboard > Projects + %li.hidden + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + Projects + + - if current_user.admin? || Gitlab::Sherlock.enabled? + %li.line-separator.hidden-xs + - if current_user.admin? + = nav_link(controller: 'admin/dashboard') do + = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('wrench fw') + - if Gitlab::Sherlock.enabled? + %li + = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions', + data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('tachometer fw') diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index 0cb367452f7..cd1c39f3226 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,30 +1,12 @@ -%ul +%ul.list-unstyled.navbar-sub-nav = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - P - %span - Projects + Projects = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - G - %span - Groups + Groups = nav_link(controller: :snippets) do = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do - .shortcut-mappings - .key - = icon('arrow-up', 'aria-label' => 'hidden') - S - %span - Snippets - %li.divider - = nav_link(controller: :help) do - = link_to help_path, title: 'Help' do - %span - Help + Snippets + %li + = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml deleted file mode 100644 index 261445ecd2b..00000000000 --- a/app/views/layouts/nav/_group.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Home' do - %span - Group - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - %span - Issues - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - %span - Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - %span - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do - %span - Settings diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml deleted file mode 100644 index cfdfcbebc9f..00000000000 --- a/app/views/layouts/nav/_new_dashboard.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -%ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects - - = nav_link(controller: ['dashboard/groups', 'explore/groups']) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do - Groups - - = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - Activity - - %li.dropdown - %a{ href: "#", data: { toggle: "dropdown" } } - More - = icon("chevron-down", class: "dropdown-chevron") - .dropdown-menu - %ul - = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do - = link_to activity_dashboard_path, title: 'Activity' do - Activity - - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do - Milestones - - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml deleted file mode 100644 index 40385f251e3..00000000000 --- a/app/views/layouts/nav/_new_explore.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -%ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do - = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - Projects - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do - Groups - %li.dropdown - %a{ href: "#", data: { toggle: "dropdown" } } - More - = icon("chevron-down", class: "dropdown-chevron") - .dropdown-menu - %ul - = nav_link(controller: :snippets) do - = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do - Snippets - %li.divider - %li - = link_to "Help", help_path, title: 'About GitLab CE' diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml deleted file mode 100644 index 448f6abedf2..00000000000 --- a/app/views/layouts/nav/_profile.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -.scrolling-tabs-container - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: 'Profile Settings' do - %span - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - %span - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do - %span - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - %span - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - %span - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - %span - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do - %span - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - %span - Notifications - - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - %span - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - %span - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - %span - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - %span - Authentication log diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml deleted file mode 100644 index b88465848e3..00000000000 --- a/app/views/layouts/nav/_project.html.haml +++ /dev/null @@ -1,111 +0,0 @@ -- can_edit = can?(current_user, :admin_project, @project) -.scrolling-tabs-container{ class: nav_control_class } - .fade-left - = icon('angle-left') - .fade-right - = icon('angle-right') - %ul.nav-links.scrolling-tabs - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - %span - Project - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do - %span - Repository - - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - %span - Registry - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - %span - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter - = number_with_delimiter(@project.open_issues_count) - - - if project_nav_tab? :merge_requests - - controllers = [:merge_requests, 'projects/merge_requests/conflicts'] - - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled? - = nav_link(controller: controllers) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - %span - Merge Requests - %span.badge.count.merge_counter.js-merge-counter - = number_with_delimiter(@project.open_merge_requests_count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - %span - Wiki - - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - %span - Snippets - - - if project_nav_tab? :project_members - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do - %span - Members - - - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - %span - Settings - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network - %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph - - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? - %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts - - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds - %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits - %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml new file mode 100644 index 00000000000..28022eebb19 --- /dev/null +++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml @@ -0,0 +1,11 @@ +- dropdown_location = local_assigns.fetch(:location, nil) +- button_tooltip = local_assigns.fetch(:title, _("Show parent pages")) +- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location) + %li.dropdown + %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } + = icon("ellipsis-h") + = icon("angle-right", class: "breadcrumbs-list-angle") + .dropdown-menu + %ul + - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| + %li{ style: "text-indent: #{[index * 16, 60].min}px;" }= link diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml new file mode 100644 index 00000000000..a7370180bf6 --- /dev/null +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -0,0 +1,15 @@ +- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted? +.projects-dropdown-container + .project-dropdown-sidebar + %ul + = nav_link(path: 'dashboard/projects#index') do + = link_to dashboard_projects_path do + = _('Your projects') + = nav_link(path: 'projects#starred') do + = link_to starred_dashboard_projects_path do + = _('Starred projects') + = nav_link(path: 'projects#trending') do + = link_to explore_root_path do + = _('Explore projects') + .project-dropdown-content + #js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } } diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 3b53117deb6..3b53117deb6 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 5a1511b262f..5a1511b262f 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index ccb6d1492f1..ccb6d1492f1 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 760c4c97c33..760c4c97c33 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index c365839e605..67aa05b655c 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,10 +1,7 @@ - page_title "User Settings" - header_title "User Settings", profile_path unless header_title - sidebar "dashboard" -- if show_new_nav? - - nav "new_profile_sidebar" - - @new_sidebar = true -- else - - nav "profile" +- nav "profile" +- @left_sidebar = true = render template: "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 54d56e9b873..6b847fb4b7c 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,11 +1,8 @@ - page_title @project.name_with_namespace - page_description @project.description unless page_description - header_title project_title(@project) unless header_title -- if show_new_nav? - - nav "new_project_sidebar" - - @new_sidebar = true -- else - - nav "project" +- nav "project" +- @left_sidebar = true - content_for :project_javascripts do - project = @target_project || @project @@ -14,12 +11,4 @@ :javascript window.uploads_path = "#{project_uploads_path(project)}"; -- content_for :header_content do - .js-dropdown-menu-projects - .dropdown-menu.dropdown-select.dropdown-menu-projects - = dropdown_title("Go to a project") - = dropdown_filter("Search your projects") - = dropdown_content - = dropdown_loading - = render template: "layouts/application" diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 985bb79508f..c606b5a1e6c 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Edit Password" - page_title "Password" - @content_class = "limit-container-width" unless fluid_layout diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 2216708d354..06bb72b9f0d 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "Access Tokens" - page_title "Personal Access Tokens" - @content_class = "limit-container-width" unless fluid_layout diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index a8ae0b92334..79f334176a5 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,8 +1,8 @@ -- breadcrumb_title "Profile" +- breadcrumb_title "Edit Profile" - @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' -= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f| += bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = form_errors(@user) .row @@ -45,12 +45,15 @@ Some options are unavailable for LDAP accounts .col-lg-8 .row - = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, - help: 'Enter your name, so people you know can recognize you.' + - if @user.read_only_attribute?(:name) + = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, + help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." + - else + = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - - if @user.external_email? - = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{email_provider_label} account." + - if @user.read_only_attribute?(:email) + = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account." - else = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?), help: user_email_help_text(@user) @@ -64,7 +67,10 @@ = f.text_field :linkedin = f.text_field :twitter = f.text_field :website_url, label: 'Website' - = f.text_field :location + - if @user.read_only_attribute?(:location) + = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account." + - else + = f.text_field :location = f.text_field :organization = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.' .prepend-top-default.append-bottom-default diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 33e062c1c9c..0b03276efcc 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -1,8 +1,5 @@ - page_title 'Two-Factor Authentication', 'Account' -- if show_new_nav? - - add_to_breadcrumbs("Account", profile_account_path) -- else - - header_title "Two-Factor Authentication", profile_two_factor_auth_path +- add_to_breadcrumbs("Account", profile_account_path) - @content_class = "limit-container-width" unless fluid_layout = render 'profiles/head' diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index f47d84ef755..0175b519867 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -1,7 +1,6 @@ - project = local_assigns.fetch(:project) -- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message -= content_for flash_message_container do += content_for :flash_message do = render partial: 'deletion_failed', locals: { project: project } - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 61420fd0fb6..e3effe45d6c 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -14,6 +14,10 @@ = form.check_box :only_allow_merge_if_all_discussions_are_resolved %strong Only allow merge requests to be merged if all discussions are resolved .checkbox + = form.label :resolve_outdated_diff_discussions do + = form.check_box :resolve_outdated_diff_discussions + %strong Automatically resolve merge request diff discussions when they become outdated + .checkbox = form.label :printing_merge_request_link_enabled do = form.check_box :printing_merge_request_link_enabled %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 5452c6db6a6..f80dadb8037 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,9 +1,7 @@ - @no_container = true -- if show_new_nav? - - add_to_breadcrumbs(_("Project"), project_path(@project)) - - page_title _("Activity") + = render "projects/head" = render 'projects/last_push' diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index a33743c2f57..4cc3218d967 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,8 +1,12 @@ +- breadcrumb_title _('Artifacts') - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' = render "projects/pipelines/head" = render "projects/jobs/header", show_controls: false +- add_to_breadcrumbs(_('Jobs'), project_jobs_path(@project)) +- add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project)) + .tree-holder .nav-block %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index c7359d873d9..60ac202bde0 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -22,7 +22,7 @@ = author_avatar(commit, size: 36) .commit-row-title %span.item-title.str-truncated-100 - = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title + = link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title .pull-right = link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha" diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 5354ec8522e..303e20e8780 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -1,11 +1,9 @@ - @no_breadcrumb_container = true - @no_container = true - @content_class = "issue-boards-content" +- breadcrumb_title "Issue Board" - page_title "Boards" -- if show_new_nav? - - add_to_breadcrumbs("Issues", project_issues_path(@project)) - - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index 18fbb81c167..7892019bb15 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -4,6 +4,6 @@ = link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha" · %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message" · #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 945a5c11d6d..73583c6bbc2 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -2,9 +2,6 @@ - page_title "Branches" = render "projects/commits/head" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - %div{ class: container_class } .top-area.adjust - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml deleted file mode 100644 index 3a73aae9d95..00000000000 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- title = capture do - .gpg-popover-icon.invalid - = render 'shared/icons/icon_status_notfound_borderless.svg' - %div - This commit was signed with an <strong>unverified</strong> signature. - -- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml new file mode 100644 index 00000000000..80eca96f7ce --- /dev/null +++ b/app/views/projects/commit/_other_user_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with a different user's verified signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml new file mode 100644 index 00000000000..e737de48e22 --- /dev/null +++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + This commit was signed with a verified signature, but the committer email + is <strong>not verified</strong> to belong to the same user. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 60fa52557ef..145bc629380 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,5 +1,2 @@ - if signature - - if signature.valid_signature? - = render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature } - - else - = render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature } + = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index d06b29db838..edff018ba6d 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,17 +1,27 @@ -- css_classes = commit_signature_badge_classes(css_classes) +- signature = local_assigns.fetch(:signature) +- title = local_assigns.fetch(:title) +- label = local_assigns.fetch(:label) +- css_class = local_assigns.fetch(:css_class) +- icon = local_assigns.fetch(:icon) +- show_user = local_assigns.fetch(:show_user, false) + +- css_classes = commit_signature_badge_classes(css_class) - title = capture do .gpg-popover-status - = title + .gpg-popover-icon{ class: css_class } + = render "shared/icons/#{icon}.svg" + %div + = title - content = capture do - .clearfix - = content + - if show_user + .clearfix + = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature } GPG Key ID: %span.monospace= signature.gpg_key_primary_keyid - = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') %button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml new file mode 100644 index 00000000000..b20198e76db --- /dev/null +++ b/app/views/projects/commit/_signature_badge_user.html.haml @@ -0,0 +1,21 @@ +- gpg_key = signature.gpg_key +- user = gpg_key&.user +- user_name = signature.gpg_key_user_name +- user_email = signature.gpg_key_user_email + +- if user + = link_to user_path(user), class: 'gpg-popover-user-link' do + %div + = user_avatar_without_link(user: user, size: 32) + + %div + %strong= user.name + %div= user.to_reference +- else + = mail_to user_email do + %div + = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) + + %div + %strong= user_name + %div= user_email diff --git a/app/views/projects/commit/_unknown_key_signature_badge.html.haml b/app/views/projects/commit/_unknown_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unknown_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_key_signature_badge.html.haml b/app/views/projects/commit/_unverified_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unverified_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml new file mode 100644 index 00000000000..1af58027b83 --- /dev/null +++ b/app/views/projects/commit/_unverified_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with an <strong>unverified</strong> signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml deleted file mode 100644 index db1a41bbf64..00000000000 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -- title = capture do - .gpg-popover-icon.valid - = render 'shared/icons/icon_status_success_borderless.svg' - %div - This commit was signed with a <strong>verified</strong> signature. - -- content = capture do - - gpg_key = signature.gpg_key - - user = gpg_key&.user - - user_name = signature.gpg_key_user_name - - user_email = signature.gpg_key_user_email - - - if user - = link_to user_path(user), class: 'gpg-popover-user-link' do - %div - = user_avatar_without_link(user: user, size: 32) - - %div - %strong= gpg_key.user.name - %div @#{gpg_key.user.username} - - else - = mail_to user_email do - %div - = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) - - %div - %strong= user_name - %div= user_email - -- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml new file mode 100644 index 00000000000..423beba2120 --- /dev/null +++ b/app/views/projects/commit/_verified_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + This commit was signed with a <strong>verified</strong> signature and the + committer email is verified to belong to the same user. + +- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 07c83c0a590..717de85c5d2 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Commits", project_commits_path(@project) +- breadcrumb_title @commit.short_id - container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : '' - limited_container_width = fluid_layout ? '' : 'limit-container-width' - @content_class = limited_container_width diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1214aabe837..b8655808d89 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -16,7 +16,7 @@ .commit-detail .commit-content - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message item-title" + = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message item-title") %span.commit-row-message.visible-xs-inline · = commit.short_id @@ -28,7 +28,8 @@ - if commit.description? %pre.commit-row-description.js-toggle-content - = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) + = preserve(markdown_field(commit, :description)) + .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - commit_timeago = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index 48cefbe45f2..26385d2f534 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -3,6 +3,6 @@ = link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha" %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message") .pull-right #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 7ae56086177..e873b931683 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -5,9 +5,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - = content_for :sub_nav do = render "head" diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 05de21e8dbf..2632fea6eba 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true +- breadcrumb_title "Compare Revisions" - page_title "Compare" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 8bc863f77b3..7cc42455394 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,8 +1,6 @@ - @no_container = true -- breadcrumb_title "Compare" +- add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project) - page_title "#{params[:from]}...#{params[:to]}" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 3467e357c49..8d008be5aae 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,7 +1,5 @@ - @no_container = true - page_title "Cycle Analytics" -- if show_new_nav? - - add_to_breadcrumbs("Project", project_path(@project)) - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('cycle_analytics') diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 4c22166c256..014486be868 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -12,6 +12,6 @@ %span.flex-truncate-child - if commit_title = deployment.commit_title = author_avatar(deployment.commit, size: 20) - = link_to_gfm commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message" + = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 02fd54c97fb..ad2d355ab4a 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -29,6 +29,6 @@ +#{diff_file.added_lines} %span.cred< \-#{diff_file.removed_lines} - %li.dropdown-menu-empty-link.hidden - %a{ href: "#" } + %li.dropdown-menu-empty-item.hidden + %a No files found. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 9e26bdecd31..994119051d2 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,3 +1,4 @@ +- breadcrumb_title "General Settings" - page_title "General" - @content_class = "limit-container-width" unless fluid_layout - expanded = Rails.env.test? diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d17709380d5..5e980314307 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,4 +1,5 @@ - @no_container = true +- breadcrumb_title "Details" = render partial: 'flash_messages', locals: { project: @project } diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index d0f723af5bf..acc80b49dd0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -1,10 +1,8 @@ - @no_container = true - page_title "Environments" +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) = render "projects/pipelines/head" -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("environments") diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 0ce0f5465fc..c35d1b5aaee 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Environments", project_environments_path(@project) +- breadcrumb_title @environment.name - page_title "Environments" = render "projects/pipelines/head" diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 9f5a1239a82..f0ef647ddb3 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,7 +1,5 @@ - @no_container = true - page_title "Charts" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - content_for :page_specific_javascripts do = webpack_bundle_tag('common_d3') = webpack_bundle_tag('graphs') diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index f41a0d8293b..08b38428b50 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -5,9 +5,6 @@ = webpack_bundle_tag('graphs') = webpack_bundle_tag('graphs_show') -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - = render 'projects/commits/head' .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 34d5a3e1831..6fb5aa45166 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -4,4 +4,4 @@ = render 'shared/empty_states/issues' - if @issues.present? - = paginate @issues, theme: "gitlab" + = paginate @issues, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index aacb057840d..6fcb5975707 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -13,15 +13,14 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render "projects/issues/nav_btns" +- content_for :breadcrumbs_extra do + = render "projects/issues/nav_btns" - if project_issues(@project).exists? %div{ class: (container_class) } .top-area = render 'shared/issuable/nav', type: :issues - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render "projects/issues/nav_btns" = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index fd7ff176c5e..fbaf88356bf 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,4 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout +- add_to_breadcrumbs "Issues", project_issues_path(@project) +- breadcrumb_title @issue.to_reference - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes @@ -37,8 +39,7 @@ %ul - if can_update_issue %li= link_to 'Edit', edit_project_issue_path(@project, @issue) - / TODO: simplify condition back #36860 - - if @issue.author && current_user != @issue.author + - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index d78891546f7..8604c7d3ea4 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -2,9 +2,6 @@ - page_title "Jobs" = render "projects/pipelines/head" -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - %div{ class: container_class } .top-area - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) } diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index fa086413fbe..975c08c06e6 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Jobs", project_jobs_path(@project) +- breadcrumb_title "##{@build.id}" - page_title "#{@build.name} (##{@build.id})", "Jobs" = render "projects/pipelines/head" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 4b9da02c6b8..ec9e8444ac5 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,7 +3,7 @@ - hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) -- if show_new_nav? && can?(current_user, :admin_label, @project) +- if can?(current_user, :admin_label, @project) - content_for :breadcrumbs_extra do = link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" @@ -18,7 +18,7 @@ Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - if can_admin_label - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to new_project_label_path(@project), class: "btn btn-new" do New label diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 4e97f74dd6a..bd6f1c05949 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,4 +5,4 @@ = render 'shared/empty_states/merge_requests' - if @merge_requests.present? - = paginate @merge_requests, theme: "gitlab" + = paginate @merge_requests, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index c020e7db380..27c3002366b 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -12,9 +12,8 @@ = webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'filtered_search' -- if show_new_nav? - - content_for :breadcrumbs_extra do - = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path +- content_for :breadcrumbs_extra do + = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'projects/last_push' @@ -22,7 +21,7 @@ %div{ class: container_class } .top-area = render 'shared/issuable/nav', type: :merge_requests - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index d27e121beb4..c2d16f7e731 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -1,4 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout +- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project) +- breadcrumb_title @merge_request.to_reference - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index e0b29b0c2e1..71ec88ef1c1 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title 'Milestones' -- if show_new_nav? && can?(current_user, :admin_milestone, @project) +- if can?(current_user, :admin_milestone, @project) - content_for :breadcrumbs_extra do = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone' @@ -11,10 +11,10 @@ .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) - .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) } + .nav-controls.nav-controls-new-nav = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-new visible-xs", title: 'New milestone' do New milestone .milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 0bf0e11c107..1f5f18801ad 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Milestones", project_milestones_path(@project) +- breadcrumb_title @milestone.title - page_title @milestone.title, "Milestones" - page_description @milestone.description = render "shared/mr_head" diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index ab948df4a3f..e29cb277389 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -2,8 +2,6 @@ - page_title "Graph", @ref - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('network') -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" = render "head" %div{ class: container_class } diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index b04f5efe1f9..de76832331a 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -1,6 +1,8 @@ -- access = note_max_access_for_user(note) -- if access - %span.note-role= access +- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR) + %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") } + = issuable_first_contribution_icon +- if access = note_max_access_for_user(note) + %span.note-role.note-role-access= Gitlab::Access.human_access(access) - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) @@ -31,7 +33,7 @@ %template{ 'v-if' => 'isResolved' } = render 'shared/icons/icon_status_success_solid.svg' %template{ 'v-else' => '' } - = render 'shared/icons/icon_status_success.svg' + = render 'shared/icons/icon_resolve_discussion.svg' - if current_user - if note.emoji_awardable? diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 7e854186973..88085c7185b 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -7,7 +7,7 @@ = custom_icon('ellipsis_v') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %li - = clipboard_button(text: noteable_note_url(note), title: "Copy reference to clipboard", button_text: 'Copy link', hide_tooltip: true, hide_button_icon: true) + = clipboard_button(text: noteable_note_url(note), title: 'Copy reference to clipboard', button_text: 'Copy link', class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true) - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml index 9b2a7b5821d..d95fa6da903 100644 --- a/app/views/projects/pipeline_schedules/edit.html.haml +++ b/app/views/projects/pipeline_schedules/edit.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project) +- breadcrumb_title "##{@schedule.id}" - page_title _("Edit"), @schedule.description, _("Pipeline Schedule") %h3.page-title diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 8426b29bb14..d9957b54a4d 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -1,4 +1,4 @@ -- breadcrumb_title "Schedules" +- breadcrumb_title _("Schedules") - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' @@ -7,12 +7,10 @@ - @no_container = true - page_title _("Pipeline Schedules") -- if show_new_nav? && can?(current_user, :create_pipeline_schedule, @project) +- if can?(current_user, :create_pipeline_schedule, @project) - content_for :breadcrumbs_extra do = link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - = render "projects/pipelines/head" %div{ class: container_class } @@ -22,7 +20,7 @@ = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope - if can?(current_user, :create_pipeline_schedule, @project) - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do %span= _('New schedule') diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml index c7237cb96d8..cfdaf6d43bb 100644 --- a/app/views/projects/pipeline_schedules/new.html.haml +++ b/app/views/projects/pipeline_schedules/new.html.haml @@ -2,8 +2,7 @@ - @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project) - page_title _("New Pipeline Schedule") -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) +- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) %h3.page-title = _("Schedule a new pipeline") diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index fd3ad69d85d..487ac87186d 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,7 +1,6 @@ - @no_container = true +- breadcrumb_title "CI / CD Charts" - page_title _("Charts"), _("Pipelines") -- if show_new_nav? - - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 63f85fc69a2..7cc9fe79afd 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Pipelines", project_pipelines_path(@project) +- breadcrumb_title "##{@pipeline.id}" - page_title "Pipeline" = render "projects/pipelines/head" diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml index 3de518c8b9a..e8028059487 100644 --- a/app/views/projects/pipelines_settings/_badge.html.haml +++ b/app/views/projects/pipelines_settings/_badge.html.haml @@ -1,34 +1,32 @@ %div{ class: badge.title.gsub(' ', '-') } - .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0 + .col-lg-12 + %h4 = badge.title.capitalize - .col-lg-8 - .prepend-top-10 - .panel.panel-default - .panel-heading - %b - = badge.title.capitalize - · - = badge.to_html - .pull-right - = render 'shared/ref_switcher', destination: 'badges', align_right: true - .panel-body - .row - .col-md-2.text-center - Markdown - .col-md-10.code.js-syntax-highlight - = highlight('.md', badge.to_markdown) - .row - %hr - .row - .col-md-2.text-center - HTML - .col-md-10.code.js-syntax-highlight - = highlight('.html', badge.to_html) - .row - %hr - .row - .col-md-2.text-center - AsciiDoc - .col-md-10.code.js-syntax-highlight - = highlight('.adoc', badge.to_asciidoc) + .panel.panel-default + .panel-heading + %b + = badge.title.capitalize + · + = badge.to_html + .pull-right + = render 'shared/ref_switcher', destination: 'badges', align_right: true + .panel-body + .row + .col-md-2.text-center + Markdown + .col-md-10.code.js-syntax-highlight + = highlight('.md', badge.to_markdown) + .row + %hr + .row + .col-md-2.text-center + HTML + .col-md-10.code.js-syntax-highlight + = highlight('.html', badge.to_html) + .row + %hr + .row + .col-md-2.text-center + AsciiDoc + .col-md-10.code.js-syntax-highlight + = highlight('.adoc', badge.to_asciidoc) diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 255d7ef38e0..8bf76b646f7 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -1,8 +1,5 @@ .row.prepend-top-default - .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0 - Pipelines - .col-lg-8 + .col-lg-12 = form_for @project, url: project_pipelines_settings_path(@project) do |f| %fieldset.builds-feature - unless @repository.gitlab_ci_yml @@ -60,8 +57,21 @@ = f.check_box :public_builds %strong Public pipelines .help-block - Allow everyone to access pipelines for public and internal projects + Allow public access to pipelines and job details, including output logs and artifacts = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' + .bs-callout.bs-callout-info + %p If enabled: + %ul + %li + For public projects, anyone can view pipelines and access job details (output logs and artifacts) + %li + For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts) + %li + For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts) + %p + If disabled, the access level will depend on the user's + permissions in the project. + %hr .form-group .checkbox diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9f7c5a315eb..25153fd0b6f 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,8 +1,5 @@ - page_title "Members" -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) - .row.prepend-top-default .col-lg-12 %h4 diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 0a5a38a3694..c786298e341 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Tags", project_tags_path(@project) +- breadcrumb_title @tag.name - page_title "Edit", @tag.name, "Tags" = render "projects/commits/head" diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index 8056217bb1e..3e2a24a4c32 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1,8 +1,6 @@ - breadcrumb_title "Integrations" - page_title @service.title, "Services" - -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" = render 'form' diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 0c4130857da..eaf374bcb83 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,12 +1,54 @@ - @content_class = "limit-container-width" unless fluid_layout -- page_title "Pipelines" - -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) +- page_title "CI / CD Settings" +- page_title "CI / CD" = render "projects/settings/head" -= render 'projects/runners/index' -= render 'ci/variables/index' -= render 'projects/triggers/index' -= render 'projects/pipelines_settings/show' +- expanded = Rails.env.test? + +%section.settings + .settings-header + %h4 + General pipelines settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Update your CI/CD configuration, like job timeout. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = render 'projects/pipelines_settings/show' + +%section.settings + .settings-header + %h4 + Runners settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Register and see your runners for this project. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = render 'projects/runners/index' + +%section.settings + .settings-header + %h4 + Secret variables + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank' + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + = render "ci/variables/content" + .settings-content.no-animate{ class: ('expanded' if expanded) } + = render 'ci/variables/index' + +%section.settings + .settings-header + %h4 + Pipeline triggers + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will + impersonate their associated user including their access to projects and their project + permissions. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = render 'projects/triggers/index' diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 149da96d3f6..933daa7f549 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,7 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout +- breadcrumb_title "Integrations Settings" - page_title 'Integrations' -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) = render "projects/settings/head" = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index cb37f3c7580..6d4af72b8ea 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -1,9 +1,7 @@ +- breadcrumb_title "Repository Settings" - page_title "Repository" - @content_class = "limit-container-width" unless fluid_layout -- if show_new_nav? - - add_to_breadcrumbs("Settings", edit_project_path(@project)) - = render "projects/settings/head" - content_for :page_specific_javascripts do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index a9b39cedb1d..3f0a24cfe83 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- breadcrumb_title "Project" +- breadcrumb_title "Details" - @content_class = "limit-container-width" unless fluid_layout = content_for :meta_tags do diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index d41cc8e0425..32844f5204a 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Snippets", project_snippets_path(@project) +- breadcrumb_title @snippet.to_reference - page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" %h3.page-title diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index ccc5fe80755..1803e7f7211 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,6 +1,6 @@ - page_title "Snippets" -- if show_new_nav? && can?(current_user, :create_project_snippet, @project) +- if can?(current_user, :create_project_snippet, @project) - content_for :breadcrumbs_extra do = link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" @@ -9,7 +9,7 @@ - include_private = @project.team.member?(current_user) || current_user.admin? = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } - .nav-controls{ class: ("visible-xs" if show_new_nav?) } + .nav-controls.visible-xs - if can?(current_user, :create_project_snippet, @project) = link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet" diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index d3e6b456f48..1359a815429 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,3 +1,5 @@ +- add_to_breadcrumbs "Snippets", project_snippets_path(@project) +- breadcrumb_title "New" - page_title "New Snippets" %h3.page-title diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index d8e448dd2af..fda068f08c2 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,4 +1,6 @@ - @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout +- add_to_breadcrumbs "Snippets", dashboard_snippets_path +- breadcrumb_title @snippet.to_reference - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" = render 'shared/snippets/header' diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 00000e0667c..a6fe02fcae0 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,11 +1,9 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title "Tags" +- add_to_breadcrumbs("Repository", project_tree_path(@project)) = render "projects/commits/head" -- if show_new_nav? - - add_to_breadcrumbs("Repository", project_tree_path(@project)) - .flex-list{ class: container_class } .top-area.adjust .nav-text.row-main-content diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index d02cd70f4c3..5d6eb4f4026 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Tags", project_tags_path(@project) +- breadcrumb_title @tag.name - page_title @tag.name, "Tags" = render "projects/commits/head" diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index f3d4706809f..abb3e918e87 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_gfm commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link" + = link_to_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link" diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 0c9c8750f2c..56197382a70 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,7 +1,7 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name = tree_icon(type, tree_item.mode, tree_item.name) - - path = flatten_tree(tree_item) + - path = flatten_tree(@path, tree_item) = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path do %span.str-truncated= path %td.hidden-xs.tree-commit diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml index ea32eac2ae2..6c2d603d95d 100644 --- a/app/views/projects/triggers/_content.html.haml +++ b/app/views/projects/triggers/_content.html.haml @@ -1,14 +1,8 @@ -%h4.prepend-top-0 - Triggers -%p.prepend-top-20 - Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will - impersonate their associated user including their access to projects and their project - permissions. -%p.prepend-top-20 +%p.append-bottom-default Triggers with the %span.label.label-primary legacy label do not have an associated user and only have access to the current project. -%p.append-bottom-0 + %br = succeed '.' do Learn more in the = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index e9a2f803edd..0f655e4ed83 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,7 +1,6 @@ .row.prepend-top-default.append-bottom-default.triggers-container - .col-lg-4 + .col-lg-12 = render "projects/triggers/content" - .col-lg-8 .panel.panel-default .panel-heading %h4.panel-title diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index dece1fad0bb..d533c611a38 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project) +- breadcrumb_title "Pages" - page_title "Pages", "Wiki" %div{ class: container_class } diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 9dadd685ea2..b066a812ec8 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,14 +1,13 @@ - @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout -- breadcrumb_title "Wiki" +- breadcrumb_title @page.title.capitalize +- wiki_breadcrumb_dropdown_links(@page.slug) - page_title @page.title.capitalize, "Wiki" +- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project) .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') - .wiki-breadcrumb - %span= breadcrumb(@page.slug) - .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 10e6c49ae9f..0ef9de5fed6 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,4 +1,4 @@ -<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> +<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36"> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg new file mode 100644 index 00000000000..fd80fd0f651 --- /dev/null +++ b/app/views/shared/icons/_caret_down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg> diff --git a/app/views/shared/icons/_icon_resolve_discussion.svg b/app/views/shared/icons/_icon_resolve_discussion.svg new file mode 100644 index 00000000000..845562e9320 --- /dev/null +++ b/app/views/shared/icons/_icon_resolve_discussion.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 845562e9320..eed5006bebe 100755 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg index 5468545da2e..0f5be6e2bc8 100644 --- a/app/views/shared/icons/_mr_bold.svg +++ b/app/views/shared/icons/_mr_bold.svg @@ -1,2 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> - +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg new file mode 100644 index 00000000000..7263d924f1f --- /dev/null +++ b/app/views/shared/icons/_plus_square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg> diff --git a/app/views/shared/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg new file mode 100644 index 00000000000..156dfa11df1 --- /dev/null +++ b/app/views/shared/icons/_todo_done.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg> diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index cb706d80f23..f16bc8dd430 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,7 +9,6 @@ class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable -- elsif issuable.author - / TODO: change back to else #36860 +- else = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index d8144a39b23..a38cd319e3c 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -37,15 +37,13 @@ %li.divider.droplab-item-ignore - / TODO: remove condition #36860 - - if issuable.author - %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), - button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } - %button.btn.btn-transparent - = icon('check', class: 'icon') - .description - %strong.title Report abuse - %p.text - Report - = display_issuable_type.pluralize - that are abusive, inappropriate or spam. + %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), + button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } + %button.btn.btn-transparent + = icon('check', class: 'icon') + .description + %strong.title Report abuse + %p.text + Report + = display_issuable_type.pluralize + that are abusive, inappropriate or spam. diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b07bc45512f..0afa48b392c 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('sidebar') -%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { "offset-top" => ("50" unless show_new_nav?), "spy" => ("affix" unless show_new_nav?), signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 7174855e176..4f00a9f2759 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -28,7 +28,7 @@ commented - if note.system %span.system-note-message - = note.redacted_note_html + = markdown_field(note, :note) %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? @@ -39,7 +39,7 @@ = render 'projects/notes/actions', note: note, note_editable: note_editable .note-body{ class: note_editable ? 'js-task-list-container' : '' } .note-text.md - = note.redacted_note_html + = markdown_field(note, :note) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago') .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } #{note.note} diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index f4f155c8d94..52a8fe8bb67 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -31,8 +31,7 @@ - if show_last_commit_as_description .description.prepend-top-5 - = link_to_gfm project.commit.title, project_commit_path(project, project.commit), - class: "commit-row-message" + = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") - elsif project.description.present? .description.prepend-top-5 = markdown_field(project, :description) diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 17b34c5eeb3..119d189f21d 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -3,10 +3,8 @@ %span.sr-only = visibility_level_label(@snippet.visibility_level) = visibility_level_icon(@snippet.visibility_level, fw: false) - %strong.item-title - Snippet #{@snippet.to_reference} %span.creator - authored + Authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 706f13dd004..578327883e5 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,5 +1,7 @@ - @hide_top_links = true - @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout +- add_to_breadcrumbs "Snippets", dashboard_snippets_path +- breadcrumb_title @snippet.to_reference - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets" = render 'shared/snippets/header' diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index f34dff2d656..9b5ff17aafa 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -6,7 +6,11 @@ class CreateGpgSignatureWorker project = Project.find_by(id: project_id) return unless project + commit = project.commit(commit_sha) + + return unless commit + # This calculates and caches the signature in the database - Gitlab::Gpg::Commit.new(project, commit_sha).signature + Gitlab::Gpg::Commit.new(commit).signature end end diff --git a/changelogs/unreleased/12968-generalize-profile-updates.yml b/changelogs/unreleased/12968-generalize-profile-updates.yml new file mode 100644 index 00000000000..d09793512c1 --- /dev/null +++ b/changelogs/unreleased/12968-generalize-profile-updates.yml @@ -0,0 +1,4 @@ +--- +title: Generalize profile updates from providers +merge_request: 12968 +author: Alexandros Keramidas diff --git a/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml b/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml new file mode 100644 index 00000000000..6d5baa8c10f --- /dev/null +++ b/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml @@ -0,0 +1,5 @@ +--- +title: Hide admin link from default search results for non-admins +merge_request: 14015 +author: +type: fixed diff --git a/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml b/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml new file mode 100644 index 00000000000..a61d703bacd --- /dev/null +++ b/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml @@ -0,0 +1,5 @@ +--- +title: Improves markdown rendering performance for commit lists. +merge_request: +author: +type: other diff --git a/changelogs/unreleased/35010-projects-nav-dropdown.yml b/changelogs/unreleased/35010-projects-nav-dropdown.yml new file mode 100644 index 00000000000..c5bed723f55 --- /dev/null +++ b/changelogs/unreleased/35010-projects-nav-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Add dropdown to Projects nav item +merge_request: 13866 +author: +type: added diff --git a/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml new file mode 100644 index 00000000000..6cd7f4e9cc6 --- /dev/null +++ b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml @@ -0,0 +1,5 @@ +--- +title: Remove project select dropdown from breadcrumb +merge_request: 14010 +author: +type: changed diff --git a/changelogs/unreleased/35161_first_time_contributor_badge.yml b/changelogs/unreleased/35161_first_time_contributor_badge.yml new file mode 100644 index 00000000000..f3ab2d9db31 --- /dev/null +++ b/changelogs/unreleased/35161_first_time_contributor_badge.yml @@ -0,0 +1,4 @@ +--- +title: "First-time contributor badge" +merge_request: 13143 +author: Micaël Bergeron <micaelbergeron@gmail.com> diff --git a/changelogs/unreleased/35441-fix-division-by-zero.yml b/changelogs/unreleased/35441-fix-division-by-zero.yml new file mode 100644 index 00000000000..335b2d40494 --- /dev/null +++ b/changelogs/unreleased/35441-fix-division-by-zero.yml @@ -0,0 +1,5 @@ +--- +title: Fix division by zero error in blame age mapping +merge_request: 13803 +author: Jeff Stubler +type: fixed diff --git a/changelogs/unreleased/35942-api-binary-encoding.yaml b/changelogs/unreleased/35942-api-binary-encoding.yaml new file mode 100644 index 00000000000..4f7960d860e --- /dev/null +++ b/changelogs/unreleased/35942-api-binary-encoding.yaml @@ -0,0 +1,3 @@ +--- +title: "Fix API to serve binary diffs that are treated as text." +merge_request: 14038 diff --git a/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml b/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml new file mode 100644 index 00000000000..54c7a8c8788 --- /dev/null +++ b/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml @@ -0,0 +1,5 @@ +--- +title: Fix new navigation wrapping and causing height to grow +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml new file mode 100644 index 00000000000..e48a5704fdd --- /dev/null +++ b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml @@ -0,0 +1,5 @@ +--- +title: Update gpg documentation with gpg2 +merge_request: 13851 +author: M M Arif +type: other diff --git a/changelogs/unreleased/36860-migrate-issues-author.yml b/changelogs/unreleased/36860-migrate-issues-author.yml new file mode 100644 index 00000000000..3e9fcc55836 --- /dev/null +++ b/changelogs/unreleased/36860-migrate-issues-author.yml @@ -0,0 +1,5 @@ +--- +title: Migrate issues authored by deleted user to the Ghost user +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml b/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml new file mode 100644 index 00000000000..83f6b2d21e1 --- /dev/null +++ b/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml @@ -0,0 +1,5 @@ +--- +title: Add repository toggle for automatically resolving outdated diff discussions +merge_request: 14053 +author: AshleyDumaine +type: added diff --git a/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml new file mode 100644 index 00000000000..fcaa6ec13f8 --- /dev/null +++ b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml @@ -0,0 +1,5 @@ +--- +title: Remove focus styles from dropdown empty links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37331-button-MR-widget.yml b/changelogs/unreleased/37331-button-MR-widget.yml new file mode 100644 index 00000000000..59bc1bd201e --- /dev/null +++ b/changelogs/unreleased/37331-button-MR-widget.yml @@ -0,0 +1,5 @@ +--- +title: Fix buttons with different height in merge request widget +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37406-success-status-icon.yml b/changelogs/unreleased/37406-success-status-icon.yml new file mode 100644 index 00000000000..faac947f188 --- /dev/null +++ b/changelogs/unreleased/37406-success-status-icon.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken svg in jobs dropdown for success status +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml new file mode 100644 index 00000000000..3e4105a4232 --- /dev/null +++ b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml @@ -0,0 +1,5 @@ +--- +title: Add quick submission on user settings page +merge_request: 14007 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/api-gpg-key-management.yml b/changelogs/unreleased/api-gpg-key-management.yml new file mode 100644 index 00000000000..0be35a5823b --- /dev/null +++ b/changelogs/unreleased/api-gpg-key-management.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Add GPG key management' +merge_request: 13828 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/api_branches_head.yml b/changelogs/unreleased/api_branches_head.yml new file mode 100644 index 00000000000..68d8d3d5168 --- /dev/null +++ b/changelogs/unreleased/api_branches_head.yml @@ -0,0 +1,5 @@ +--- +title: Add branch existence check to the APIv4 branches via HEAD request +merge_request: 13979 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/changelogs/unreleased/collapsable-pipeline-settings.yml b/changelogs/unreleased/collapsable-pipeline-settings.yml new file mode 100644 index 00000000000..d41959f8ab0 --- /dev/null +++ b/changelogs/unreleased/collapsable-pipeline-settings.yml @@ -0,0 +1,5 @@ +--- +title: Add collapsable sections for Pipeline Settings +merge_request: +author: +type: added diff --git a/changelogs/unreleased/feature-dependency-status-badge.yml b/changelogs/unreleased/feature-dependency-status-badge.yml new file mode 100644 index 00000000000..1becff3585a --- /dev/null +++ b/changelogs/unreleased/feature-dependency-status-badge.yml @@ -0,0 +1,5 @@ +--- +title: Add badge for dependency status +merge_request: 13588 +author: Markus Koller +type: other diff --git a/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml b/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml new file mode 100644 index 00000000000..920679ca166 --- /dev/null +++ b/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml @@ -0,0 +1,5 @@ +--- +title: Make it possible to download a single job artifact file using the API +merge_request: 14027 +author: +type: added diff --git a/changelogs/unreleased/feature-gpg-verification-status.yml b/changelogs/unreleased/feature-gpg-verification-status.yml new file mode 100644 index 00000000000..7518fafcdb8 --- /dev/null +++ b/changelogs/unreleased/feature-gpg-verification-status.yml @@ -0,0 +1,6 @@ +--- +title: 'Update the GPG verification semantics: A GPG signature must additionally match + the committer in order to be verified' +merge_request: 13771 +author: Alexis Reigel +type: changed diff --git a/changelogs/unreleased/fix-import-export-performance.yml b/changelogs/unreleased/fix-import-export-performance.yml new file mode 100644 index 00000000000..1f59c4eb179 --- /dev/null +++ b/changelogs/unreleased/fix-import-export-performance.yml @@ -0,0 +1,5 @@ +--- +title: Improve Import/Export memory usage +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix_wiki_toc_indent.yml b/changelogs/unreleased/fix_wiki_toc_indent.yml new file mode 100644 index 00000000000..60da2e455f2 --- /dev/null +++ b/changelogs/unreleased/fix_wiki_toc_indent.yml @@ -0,0 +1,5 @@ +--- +title: Wiki table of contents are now properly nested to reflect header level +merge_request: 13650 +author: Akihiro Nakashima +type: fixed diff --git a/changelogs/unreleased/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml new file mode 100644 index 00000000000..8195e97ed59 --- /dev/null +++ b/changelogs/unreleased/fuzzy-issue-search.yml @@ -0,0 +1,5 @@ +--- +title: Support a multi-word fuzzy seach issues/merge requests on search bar +merge_request: 13780 +author: Hiroyuki Sato +type: changed diff --git a/changelogs/unreleased/mr-index-page-performance.yml b/changelogs/unreleased/mr-index-page-performance.yml new file mode 100644 index 00000000000..df5f44c04fa --- /dev/null +++ b/changelogs/unreleased/mr-index-page-performance.yml @@ -0,0 +1,5 @@ +--- +title: Re-use issue/MR counts for the pagination system +merge_request: +author: +type: other diff --git a/changelogs/unreleased/sh-bump-jira-gem.yml b/changelogs/unreleased/sh-bump-jira-gem.yml new file mode 100644 index 00000000000..d76b688caac --- /dev/null +++ b/changelogs/unreleased/sh-bump-jira-gem.yml @@ -0,0 +1,5 @@ +--- +title: Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/url-sanitizer-fixes.yml b/changelogs/unreleased/url-sanitizer-fixes.yml new file mode 100644 index 00000000000..769036c829c --- /dev/null +++ b/changelogs/unreleased/url-sanitizer-fixes.yml @@ -0,0 +1,5 @@ +--- +title: Fix problems sanitizing URLs with empty passwords +merge_request: 14083 +author: +type: fixed diff --git a/changelogs/unreleased/winh-dropdown-changelog-docs.yml b/changelogs/unreleased/winh-dropdown-changelog-docs.yml new file mode 100644 index 00000000000..2f42b4dd9f9 --- /dev/null +++ b/changelogs/unreleased/winh-dropdown-changelog-docs.yml @@ -0,0 +1,5 @@ +--- +title: Restyle dropdown menus to make them look consistent +merge_request: +author: +type: other diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c5704ac5857..e9661090844 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -372,9 +372,16 @@ production: &base # showing GitLab's sign-in page (default: show the GitLab sign-in page) # auto_sign_in_with_provider: saml - # Sync user's email address from the specified Omniauth provider every time the user logs - # in (default: nil). And consequently make this field read-only. - # sync_email_from_provider: cas3 + # Sync user's profile from the specified Omniauth providers every time the user logs in (default: empty). + # Define the allowed providers using an array, e.g. ["cas3", "saml", "twitter"], + # or as true/false to allow all providers or none. + # sync_profile_from_provider: [] + + # Select which info to sync from the providers above. (default: email). + # Define the synced profile info using an array. Available options are "name", "email" and "location" + # e.g. ["name", "email", "location"] or as true to sync all available. + # This consequently will make the selected attributes read-only. + # sync_profile_attributes: true # CAUTION! # This allows users to login without having a user account first. Define the allowed providers diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 360b72cdea3..7c1ca05a57b 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -173,7 +173,20 @@ Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_prov Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? -Settings.omniauth['sync_email_from_provider'] ||= nil + +Settings.omniauth['sync_profile_from_provider'] = false if Settings.omniauth['sync_profile_from_provider'].nil? +Settings.omniauth['sync_profile_attributes'] = ['email'] if Settings.omniauth['sync_profile_attributes'].nil? + +# Handle backwards compatibility with merge request 11268 +if Settings.omniauth['sync_email_from_provider'] + if Settings.omniauth['sync_profile_from_provider'].is_a?(Array) + Settings.omniauth['sync_profile_from_provider'] |= [Settings.omniauth['sync_email_from_provider']] + elsif !Settings.omniauth['sync_profile_from_provider'] + Settings.omniauth['sync_profile_from_provider'] = [Settings.omniauth['sync_email_from_provider']] + end + + Settings.omniauth['sync_profile_attributes'] |= ['email'] unless Settings.omniauth['sync_profile_attributes'] == true +end Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index 5b455a8065a..e1a59d8c152 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -114,9 +114,6 @@ def instrument_classes(instrumentation) # This is a Rails scope so we have to instrument it manually. instrumentation.instrument_method(Project, :visible_to_user) - # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/34509 - instrumentation.instrument_method(MarkupHelper, :link_to_gfm) - # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159 instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits) diff --git a/config/webpack.config.js b/config/webpack.config.js index ad88e48550d..6b0cd023291 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -30,7 +30,7 @@ var config = { blob: './blob_edit/blob_bundle.js', boards: './boards/boards_bundle.js', common: './commons/index.js', - common_vue: ['vue', './vue_shared/common_vue.js'], + common_vue: './vue_shared/vue_resource_interceptor.js', common_d3: ['d3'], cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff --git a/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb b/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb new file mode 100644 index 00000000000..128cd109f8d --- /dev/null +++ b/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb @@ -0,0 +1,20 @@ +class AddVerificationStatusToGpgSignatures < ActiveRecord::Migration + DOWNTIME = false + + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + # First we remove all signatures because we need to re-verify them all + # again anyway (because of the updated verification logic). + # + # This makes adding the column with default values faster + truncate(:gpg_signatures) + + add_column_with_default(:gpg_signatures, :verification_status, :smallint, default: 0) + end + + def down + remove_column(:gpg_signatures, :verification_status) + end +end diff --git a/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb b/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb new file mode 100644 index 00000000000..79028e34987 --- /dev/null +++ b/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb @@ -0,0 +1,15 @@ +class CreateUserSyncedAttributesMetadata < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :user_synced_attributes_metadata do |t| + t.boolean :name_synced, default: false + t.boolean :email_synced, default: false + t.boolean :location_synced, default: false + t.references :user, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.string :provider + end + end +end diff --git a/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb new file mode 100644 index 00000000000..c5fb5762d61 --- /dev/null +++ b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb @@ -0,0 +1,37 @@ +class MigrateIssuesToGhostUser < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + end + + class Issue < ActiveRecord::Base + self.table_name = 'issues' + + include ::EachBatch + end + + def reset_column_in_migration_models + ActiveRecord::Base.clear_cache! + + ::User.reset_column_information + ::Namespace.reset_column_information + end + + def up + reset_column_in_migration_models + + # we use the model method because rewriting it is too complicated and would require copying multiple methods + ghost_id = ::User.ghost.id + + Issue.where('NOT EXISTS (?)', User.unscoped.select(1).where('issues.author_id = users.id')).each_batch do |relation| + relation.update_all(author_id: ghost_id) + end + end + + def down + end +end diff --git a/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb b/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb new file mode 100644 index 00000000000..235530bb1e6 --- /dev/null +++ b/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb @@ -0,0 +1,9 @@ +class ResolveOutdatedDiffDiscussions < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:projects, :resolve_outdated_diff_discussions, :boolean) + end +end diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb new file mode 100644 index 00000000000..592e141b7e6 --- /dev/null +++ b/db/migrate/20170828135939_migrate_user_external_mail_data.rb @@ -0,0 +1,57 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateUserExternalMailData < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + + include EachBatch + end + + class UserSyncedAttributesMetadata < ActiveRecord::Base + self.table_name = 'user_synced_attributes_metadata' + + include EachBatch + end + + def up + User.each_batch do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + execute <<-EOF + INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced) + SELECT id, email_provider, external_email + FROM users + WHERE external_email = TRUE + AND NOT EXISTS ( + SELECT true + FROM user_synced_attributes_metadata + WHERE user_id = users.id + AND provider = users.email_provider + ) + AND id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + + def down + UserSyncedAttributesMetadata.each_batch do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + execute <<-EOF + UPDATE users + SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced + FROM user_synced_attributes_metadata as metadata, users + WHERE metadata.email_synced = TRUE + AND metadata.user_id = users.id + AND id BETWEEN #{start_id} AND #{end_id} + EOF + end + end +end diff --git a/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb new file mode 100644 index 00000000000..ab6e9fb565a --- /dev/null +++ b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb @@ -0,0 +1,14 @@ +class AddForeignKeyToIssueAuthor < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + disable_ddl_transaction! + + def up + add_concurrent_foreign_key(:issues, :users, column: :author_id, on_delete: :nullify) + end + + def down + remove_foreign_key(:issues, column: :author_id) + end +end diff --git a/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb b/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb new file mode 100644 index 00000000000..ceb31ffb08a --- /dev/null +++ b/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb @@ -0,0 +1,9 @@ +class AddResolvedByPushToNotes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :notes, :resolved_by_push, :boolean + end +end diff --git a/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb new file mode 100644 index 00000000000..fefd931e5d2 --- /dev/null +++ b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb @@ -0,0 +1,57 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + + include EachBatch + end + + class UserSyncedAttributesMetadata < ActiveRecord::Base + self.table_name = 'user_synced_attributes_metadata' + + include EachBatch + end + + def up + User.each_batch do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + execute <<-EOF + INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced) + SELECT id, email_provider, external_email + FROM users + WHERE external_email = TRUE + AND NOT EXISTS ( + SELECT true + FROM user_synced_attributes_metadata + WHERE user_id = users.id + AND provider = users.email_provider + ) + AND id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + + def down + UserSyncedAttributesMetadata.each_batch do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + execute <<-EOF + UPDATE users + SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced + FROM user_synced_attributes_metadata as metadata, users + WHERE metadata.email_synced = TRUE + AND metadata.user_id = users.id + AND id BETWEEN #{start_id} AND #{end_id} + EOF + end + end +end diff --git a/db/post_migrate/20170828170513_remove_user_email_provider_column.rb b/db/post_migrate/20170828170513_remove_user_email_provider_column.rb new file mode 100644 index 00000000000..570f2b3772a --- /dev/null +++ b/db/post_migrate/20170828170513_remove_user_email_provider_column.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUserEmailProviderColumn < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :users, :email_provider, :string + end +end diff --git a/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb b/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb new file mode 100644 index 00000000000..bb81dc682b3 --- /dev/null +++ b/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUserExternalMailColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :users, :external_email, :boolean + end +end diff --git a/db/post_migrate/20170830084744_destroy_gpg_signatures.rb b/db/post_migrate/20170830084744_destroy_gpg_signatures.rb new file mode 100644 index 00000000000..b04d36f6537 --- /dev/null +++ b/db/post_migrate/20170830084744_destroy_gpg_signatures.rb @@ -0,0 +1,10 @@ +class DestroyGpgSignatures < ActiveRecord::Migration + DOWNTIME = false + + def up + truncate(:gpg_signatures) + end + + def down + end +end diff --git a/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb b/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb new file mode 100644 index 00000000000..9b6745e33d9 --- /dev/null +++ b/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb @@ -0,0 +1,11 @@ +class RemoveValidSignatureFromGpgSignatures < ActiveRecord::Migration + DOWNTIME = false + + def up + remove_column :gpg_signatures, :valid_signature + end + + def down + add_column :gpg_signatures, :valid_signature, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 216b3cddab0..1c1a5e63bc4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170830125940) do +ActiveRecord::Schema.define(version: 20170905112933) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -609,11 +609,11 @@ ActiveRecord::Schema.define(version: 20170830125940) do t.datetime "updated_at", null: false t.integer "project_id" t.integer "gpg_key_id" - t.boolean "valid_signature" t.binary "commit_sha" t.binary "gpg_key_primary_keyid" t.text "gpg_key_user_name" t.text "gpg_key_user_email" + t.integer "verification_status", limit: 2, default: 0, null: false end add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree @@ -1002,6 +1002,7 @@ ActiveRecord::Schema.define(version: 20170830125940) do t.text "note_html" t.integer "cached_markdown_version" t.text "change_position" + t.boolean "resolved_by_push" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -1219,6 +1220,7 @@ ActiveRecord::Schema.define(version: 20170830125940) do t.string "ci_config_path" t.text "delete_error" t.integer "storage_version", limit: 2 + t.boolean "resolve_outdated_diff_discussions" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1537,6 +1539,16 @@ ActiveRecord::Schema.define(version: 20170830125940) do add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "user_synced_attributes_metadata", force: :cascade do |t| + t.boolean "name_synced", default: false + t.boolean "email_synced", default: false + t.boolean "location_synced", default: false + t.integer "user_id", null: false + t.string "provider" + end + + add_index "user_synced_attributes_metadata", ["user_id"], name: "index_user_synced_attributes_metadata_on_user_id", unique: true, using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -1602,8 +1614,6 @@ ActiveRecord::Schema.define(version: 20170830125940) do t.boolean "notified_of_own_activity" t.string "preferred_language" t.string "rss_token" - t.boolean "external_email", default: false, null: false - t.string "email_provider" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -1708,6 +1718,7 @@ ActiveRecord::Schema.define(version: 20170830125940) do add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade + add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade @@ -1753,6 +1764,7 @@ ActiveRecord::Schema.define(version: 20170830125940) do add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" + add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 63ba8ff03e9..b250fa08382 100644 --- a/doc/README.md +++ b/doc/README.md @@ -160,7 +160,6 @@ have access to GitLab administration tools and settings. ### Integrations - [Integrations](integration/README.md): How to integrate with systems such as JIRA, Redmine, Twitter. -- [Koding](administration/integration/koding.md): Set up Koding to use with GitLab. - [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost. ### Monitoring diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md index b95c425842c..67f9f01efb8 100644 --- a/doc/administration/integration/koding.md +++ b/doc/administration/integration/koding.md @@ -1,6 +1,10 @@ # Koding & GitLab -> [Introduced][ce-5909] in GitLab 8.11. +>**Notes:** +- **As of GitLab 10.0, the Koding integration is deprecated and will be removed + in a future version. The option to configure it is removed from GitLab's admin + area.** +- [Introduced][ce-5909] in GitLab 8.11. This document will guide you through installing and configuring Koding with GitLab. diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md index ee680c7b258..68efe0aae5c 100644 --- a/doc/administration/monitoring/performance/performance_bar.md +++ b/doc/administration/monitoring/performance/performance_bar.md @@ -5,17 +5,17 @@ activated, it looks as follows: ![Performance Bar](img/performance_bar.png) -It allows you to: +It allows you to see (from left to right): -- see the current host serving the page -- see the timing of the page (backend, frontend) -- the number of DB queries, the time it took, and the detail of these queries +- the current host serving the page +- the timing of the page (backend, frontend) +- time taken and number of DB queries, click through for details of these queries ![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png) -- the number of calls to Redis, and the time it took -- the number of background jobs created by Sidekiq, and the time it took -- the number of Ruby GC calls, and the time it took -- profile the code used to generate the page, line by line +- time taken and number of calls to Redis +- time taken and number of background jobs created by Sidekiq +- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)). ![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png) +- time taken and number of Ruby GC calls ## Enable the Performance Bar via the Admin panel diff --git a/doc/api/README.md b/doc/api/README.md index c2a08dcff07..db61497db53 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -61,16 +61,7 @@ following locations: ## Road to GraphQL -Going forward, we will start on moving to -[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of -controller-specific endpoints. GraphQL has a number of benefits: - -1. We avoid having to maintain two different APIs. -2. Callers of the API can request only what they need. -3. It is versioned by default. - -It will co-exist with the current v4 REST API. If we have a v5 API, this should -be a compatibility layer on top of GraphQL. +We have changed our plans to move to GraphQL. After reviewing the GraphQL license, anything related to the Facebook BSD plus patent license will not be allowed at GitLab. ## Basic usage @@ -246,8 +237,8 @@ The following table gives an overview of how the API functions generally behave. | ------------ | ----------- | | `GET` | Access one or more resources and return the result as JSON. | | `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. | -| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. | -| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. | +| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. | +| `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. | The following table shows the possible return codes for API requests. diff --git a/doc/api/environments.md b/doc/api/environments.md index 5ca766bf87d..e8deb3e07e9 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -94,7 +94,7 @@ Example response: ## Delete an environment -It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist. +It returns `204` if the environment was successfully deleted, and `404` if the environment does not exist. ``` DELETE /projects/:id/environments/:environment_id diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 297115e94ac..d60c7c12881 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -320,11 +320,11 @@ Response: [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 -## Download the artifacts file +## Download the artifacts archive > [Introduced][ce-5347] in GitLab 8.10. -Download the artifacts file from the given reference name and job provided the +Download the artifacts archive from the given reference name and job provided the job finished successfully. ``` @@ -354,6 +354,40 @@ Example response: [ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347 +## Download a single artifact file + +> Introduced in GitLab 10.0 + +Download a single artifact file from within the job's artifacts archive. + +Only a single file is going to be extracted from the archive and streamed to a client. + +``` +GET /projects/:id/jobs/:job_id/artifacts/*artifact_path +``` + +Parameters + +| Attribute | Type | Required | Description | +|-----------------|---------|----------|-------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `job_id ` | integer | yes | The unique job identifier | +| `artifact_path` | string | yes | Path to a file inside the artifacts archive | + +Example request: + +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/5/artifacts/some/release/file.pdf" +``` + +Example response: + +| Status | Description | +|-----------|--------------------------------------| +| 200 | Sends a single artifact file | +| 400 | Invalid path provided | +| 404 | Build not found or no file/artifacts | + ## Get a trace file Get a trace of a specific job of a project diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index 24c8ff5fa7a..ad2521230e6 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -95,8 +95,7 @@ Parameters: ## Delete snippet -Deletes an existing project snippet. This is an idempotent function and deleting a non-existent -snippet still returns a `200 OK` status code. +Deletes an existing project snippet. This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found. ``` DELETE /projects/:id/snippets/:snippet_id diff --git a/doc/api/projects.md b/doc/api/projects.md index d3f8e509612..3144220e588 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,6 +1,6 @@ # Projects API -### Project visibility level +## Project visibility level Project in GitLab can be either private, internal or public. This is determined by the `visibility` field in the project. @@ -16,16 +16,15 @@ Values for the project visibility level are: * `public`: The project can be cloned without any authentication. -## List projects +## List all projects -Get a list of visible projects for authenticated user. When accessed without authentication, only public projects are returned. +Get a list of all visible projects across GitLab for the authenticated user. +When accessed without authentication, only public projects are returned. ``` GET /projects ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `archived` | boolean | no | Limit by archived status | @@ -70,6 +69,7 @@ Parameters: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -137,6 +137,7 @@ Parameters: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -191,16 +192,15 @@ Parameters: ] ``` -### List a user's projects +## List user projects -Get a list of visible projects for the given user. When accessed without authentication, only public projects are returned. +Get a list of visible projects for the given user. When accessed without +authentication, only public projects are returned. ``` GET /users/:user_id/projects ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `user_id` | string | yes | The ID or username of the user | @@ -246,6 +246,7 @@ Parameters: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -313,6 +314,7 @@ Parameters: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -367,7 +369,7 @@ Parameters: ] ``` -### Get single project +## Get single project Get a specific project. This endpoint can be accessed without authentication if the project is publicly accessible. @@ -376,8 +378,6 @@ the project is publicly accessible. GET /projects/:id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -411,6 +411,7 @@ Parameters: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -480,17 +481,14 @@ Parameters: Get the users list of a project. - -Parameters: +``` +GET /projects/:id/users +``` | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `search` | string | no | Search for specific users | -``` -GET /projects/:id/users -``` - ```json [ { @@ -512,11 +510,11 @@ GET /projects/:id/users ] ``` -### Get project events +## Get project events -Please refer to the [Events API documentation](events.md#list-a-projects-visible-events) +Please refer to the [Events API documentation](events.md#list-a-projects-visible-events). -### Create project +## Create project Creates a new project owned by the authenticated user. @@ -524,8 +522,6 @@ Creates a new project owned by the authenticated user. POST /projects ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. | @@ -537,6 +533,7 @@ Parameters: | `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | +| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | @@ -551,7 +548,7 @@ Parameters: | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | | `ci_config_path` | string | no | The path to CI config file | -### Create project for user +## Create project for user Creates a new project owned by the specified user. Available only for admins. @@ -559,8 +556,6 @@ Creates a new project owned by the specified user. Available only for admins. POST /projects/user/:user_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `user_id` | integer | yes | The user ID of the project owner | @@ -574,6 +569,7 @@ Parameters: | `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | +| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | @@ -588,7 +584,7 @@ Parameters: | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | | `ci_config_path` | string | no | The path to CI config file | -### Edit project +## Edit project Updates an existing project. @@ -596,8 +592,6 @@ Updates an existing project. PUT /projects/:id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -610,6 +604,7 @@ Parameters: | `jobs_enabled` | boolean | no | Enable jobs for this project | | `wiki_enabled` | boolean | no | Enable wiki for this project | | `snippets_enabled` | boolean | no | Enable snippets for this project | +| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `visibility` | string | no | See [project visibility level](#project-visibility-level) | @@ -623,24 +618,24 @@ Parameters: | `avatar` | mixed | no | Image file for avatar of the project | | `ci_config_path` | string | no | The path to CI config file | -### Fork project +## Fork project Forks a project into the user namespace of the authenticated user or the one provided. -The forking operation for a project is asynchronous and is completed in a background job. The request will return immediately. To determine whether the fork of the project has completed, query the `import_status` for the new project. +The forking operation for a project is asynchronous and is completed in a +background job. The request will return immediately. To determine whether the +fork of the project has completed, query the `import_status` for the new project. ``` POST /projects/:id/fork ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to | -### Star a project +## Star a project Stars a given project. Returns status code `304` if the project is already starred. @@ -648,8 +643,6 @@ Stars a given project. Returns status code `304` if the project is already starr POST /projects/:id/star ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -683,6 +676,7 @@ Example response: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -717,7 +711,7 @@ Example response: } ``` -### Unstar a project +## Unstar a project Unstars a given project. Returns status code `304` if the project is not starred. @@ -758,6 +752,7 @@ Example response: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -792,7 +787,7 @@ Example response: } ``` -### Archive a project +## Archive a project Archives the project if the user is either admin or the project owner of this project. This action is idempotent, thus archiving an already archived project will not change the project. @@ -839,6 +834,7 @@ Example response: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -885,7 +881,7 @@ Example response: } ``` -### Unarchive a project +## Unarchive a project Unarchives the project if the user is either admin or the project owner of this project. This action is idempotent, thus unarchiving an non-archived project will not change the project. @@ -932,6 +928,7 @@ Example response: "jobs_enabled": true, "wiki_enabled": true, "snippets_enabled": false, + "resolve_outdated_diff_discussions": false, "container_registry_enabled": false, "created_at": "2013-09-30T13:46:02Z", "last_activity_at": "2013-09-30T13:46:02Z", @@ -978,7 +975,7 @@ Example response: } ``` -### Remove project +## Remove project Removes a project including all associated resources (issues, merge requests etc.) @@ -986,15 +983,11 @@ Removes a project including all associated resources (issues, merge requests etc DELETE /projects/:id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | -## Uploads - -### Upload a file +## Upload a file Uploads a file to the specified project to be used in an issue or merge request description, or a comment. @@ -1002,8 +995,6 @@ Uploads a file to the specified project to be used in an issue or merge request POST /projects/:id/uploads ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1028,15 +1019,11 @@ Returned object: } ``` -**Note**: The returned `url` is relative to the project path. +>**Note**: The returned `url` is relative to the project path. In Markdown contexts, the link is automatically expanded when the format in `markdown` is used. -## Project members - -Please consult the [Project Members](members.md) documentation. - -### Share project with group +## Share project with group Allow to share project with group. @@ -1044,8 +1031,6 @@ Allow to share project with group. POST /projects/:id/share ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1053,7 +1038,7 @@ Parameters: | `group_access` | integer | yes | The permissions level to grant the group | | `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 | -### Delete a shared project link within a group +## Delete a shared project link within a group Unshare the project from the group. Returns `204` and no content on success. @@ -1061,8 +1046,6 @@ Unshare the project from the group. Returns `204` and no content on success. DELETE /projects/:id/share/:group_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1085,8 +1068,6 @@ Get a list of project hooks. GET /projects/:id/hooks ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1099,8 +1080,6 @@ Get a specific hook for a project. GET /projects/:id/hooks/:hook_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1132,8 +1111,6 @@ Adds a hook to a specified project. POST /projects/:id/hooks ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1157,8 +1134,6 @@ Edits a hook for a specified project. PUT /projects/:id/hooks/:hook_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1184,8 +1159,6 @@ Either the hook is available or not. DELETE /projects/:id/hooks/:hook_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1194,126 +1167,16 @@ Parameters: Note the JSON response differs if the hook is available or not. If the project hook is available before it is returned in the JSON response or an empty response is returned. -## Branches - -For more information please consult the [Branches](branches.md) documentation. - -### List branches - -Lists all branches of a project. - -``` -GET /projects/:id/repository/branches -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | - -```json -[ - { - "name": "async", - "commit": { - "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca", - "parent_ids": [ - "3f94fc7c85061973edc9906ae170cc269b07ca55" - ], - "message": "give Caolan credit where it's due (up top)", - "author_name": "Jeremy Ashkenas", - "author_email": "jashkenas@example.com", - "authored_date": "2010-12-08T21:28:50+00:00", - "committer_name": "Jeremy Ashkenas", - "committer_email": "jashkenas@example.com", - "committed_date": "2010-12-08T21:28:50+00:00" - }, - "protected": false, - "developers_can_push": false, - "developers_can_merge": false - }, - { - "name": "gh-pages", - "commit": { - "id": "101c10a60019fe870d21868835f65c25d64968fc", - "parent_ids": [ - "9c15d2e26945a665131af5d7b6d30a06ba338aaa" - ], - "message": "Underscore.js 1.5.2", - "author_name": "Jeremy Ashkenas", - "author_email": "jashkenas@example.com", - "authored_date": "2013-09-07T12:58:21+00:00", - "committer_name": "Jeremy Ashkenas", - "committer_email": "jashkenas@example.com", - "committed_date": "2013-09-07T12:58:21+00:00" - }, - "protected": false, - "developers_can_push": false, - "developers_can_merge": false - } -] -``` - -### Single branch - -A specific branch of a project. - -``` -GET /projects/:id/repository/branches/:branch -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | -| `branch` | string | yes | The name of the branch | -| `developers_can_push` | boolean | no | Flag if developers can push to the branch | -| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch | - -### Protect single branch - -Protects a single branch of a project. - -``` -PUT /projects/:id/repository/branches/:branch/protect -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | -| `branch` | string | yes | The name of the branch | - -### Unprotect single branch - -Unprotects a single branch of a project. - -``` -PUT /projects/:id/repository/branches/:branch/unprotect -``` - -Parameters: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | -| `branch` | string | yes | The name of the branch | - ## Admin fork relation Allows modification of the forked relationship between existing projects. Available only for admins. -### Create a forked from/to relation between existing projects. +### Create a forked from/to relation between existing projects ``` POST /projects/:id/fork/:forked_from_id ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1325,8 +1188,6 @@ Parameters: DELETE /projects/:id/fork ``` -Parameter: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | @@ -1341,8 +1202,6 @@ accessible. GET /projects ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `search` | string | yes | A string contained in the project name | @@ -1355,14 +1214,20 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a ## Start the Housekeeping task for a Project ->**Note:** This feature was introduced in GitLab 9.0 +> Introduced in GitLab 9.0. ``` POST /projects/:id/housekeeping ``` -Parameters: - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | + +## Branches + +Read more in the [Branches](branches.md) documentation. + +## Project members + +Read more in the [Project members](members.md) documentation. diff --git a/doc/api/users.md b/doc/api/users.md index 57a13eb477d..9f3e4caf2f4 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -299,10 +299,7 @@ e.g. when renaming the email address to some existing one. ## User deletion Deletes a user. Available only for administrators. -This is an idempotent function, calling this function for a non-existent user id -still returns a status code `200 OK`. -The JSON response differs if the user was actually deleted or not. -In the former the user is returned and in the latter not. +This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found. ``` DELETE /users/:id @@ -524,8 +521,7 @@ Parameters: ## Delete SSH key for current user Deletes key owned by currently authenticated user. -This is an idempotent function and calling it on a key that is already deleted -or not available results in `200 OK`. +This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found. ``` DELETE /user/keys/:key_id @@ -548,7 +544,216 @@ Parameters: - `id` (required) - id of specified user - `key_id` (required) - SSH key ID -Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found. +## List all GPG keys + +Get a list of currently authenticated user's GPG keys. + +``` +GET /user/gpg_keys +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys +``` + +Example response: + +```json +[ + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +] +``` + +## Get a specific GPG key + +Get a specific GPG key of currently authenticated user. + +``` +GET /user/gpg_keys/:key_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `key_id` | integer | yes | The ID of the GPG key | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1 +``` + +Example response: + +```json + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +``` + +## Add a GPG key + +Creates a new GPG key owned by the currently authenticated user. + +``` +POST /user/gpg_keys +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| key | string | yes | The new GPG key | + +```bash +curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys +``` + +Example response: + +```json +[ + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +] +``` + +## Delete a GPG key + +Delete a GPG key owned by currently authenticated user. + +``` +DELETE /user/gpg_keys/:key_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `key_id` | integer | yes | The ID of the GPG key | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1 +``` + +Returns `204 No Content` on success, or `404 Not found` if the key cannot be found. + +## List all GPG keys for given user + +Get a list of a specified user's GPG keys. Available only for admins. + +``` +GET /users/:id/gpg_keys +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys +``` + +Example response: + +```json +[ + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +] +``` + +## Get a specific GPG key for a given user + +Get a specific GPG key for a given user. Available only for admins. + +``` +GET /users/:id/gpg_keys/:key_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1 +``` + +Example response: + +```json + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +``` + +## Add a GPG key for a given user + +Create new GPG key owned by the specified user. Available only for admins. + +``` +POST /users/:id/gpg_keys +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | + +```bash +curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys +``` + +Example response: + +```json +[ + { + "id": 1, + "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----", + "created_at": "2017-09-05T09:17:46.264Z" + } +] +``` + +## Delete a GPG key for a given user + +Delete a GPG key owned by a specified user. Available only for admins. + +``` +DELETE /users/:id/gpg_keys/:key_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1 +``` ## List emails @@ -654,8 +859,7 @@ Parameters: ## Delete email for current user Deletes email owned by currently authenticated user. -This is an idempotent function and calling it on a email that is already deleted -or not available results in `200 OK`. +This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found. ``` DELETE /user/emails/:email_id @@ -678,8 +882,6 @@ Parameters: - `id` (required) - id of specified user - `email_id` (required) - email ID -Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found. - ## Block user Blocks the specified user. Available only for admin. diff --git a/doc/ci/autodeploy/img/auto_deploy_btn.png b/doc/ci/autodeploy/img/auto_deploy_btn.png Binary files differnew file mode 100644 index 00000000000..25915ed1c9d --- /dev/null +++ b/doc/ci/autodeploy/img/auto_deploy_btn.png diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png Binary files differindex b93b0a08fea..5815937a4af 100644 --- a/doc/ci/autodeploy/img/auto_deploy_dropdown.png +++ b/doc/ci/autodeploy/img/auto_deploy_dropdown.png diff --git a/doc/ci/autodeploy/img/guide_connect_cluster.png b/doc/ci/autodeploy/img/guide_connect_cluster.png Binary files differnew file mode 100644 index 00000000000..b856b81a1d0 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_connect_cluster.png diff --git a/doc/ci/autodeploy/img/guide_integration.png b/doc/ci/autodeploy/img/guide_integration.png Binary files differnew file mode 100644 index 00000000000..723b2619ea2 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_integration.png diff --git a/doc/ci/autodeploy/img/guide_secret.png b/doc/ci/autodeploy/img/guide_secret.png Binary files differnew file mode 100644 index 00000000000..01f5aa49908 --- /dev/null +++ b/doc/ci/autodeploy/img/guide_secret.png diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index a714689ebd5..a128cf69c20 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -1,8 +1,13 @@ -# Auto deploy +# Auto Deploy -> [Introduced][mr-8135] in GitLab 8.15. -> Auto deploy is an experimental feature and is not recommended for Production use at this time. -> As of GitLab 9.1, access to the container registry is only available while the Pipeline is running. Restarting a pod, scaling a service, or other actions which require on-going access will fail. On-going secure access is planned for a subsequent release. +>**Notes:** +- [Introduced][mr-8135] in GitLab 8.15. +- Auto deploy is an experimental feature and is not recommended for Production + use at this time. +- As of GitLab 9.1, access to the Container Registry is only available while + the Pipeline is running. Restarting a pod, scaling a service, or other actions + which require on-going access will fail. On-going secure access is planned for + a subsequent release. Auto deploy is an easy way to configure GitLab CI for the deployment of your application. GitLab Community maintains a list of `.gitlab-ci.yml` @@ -11,9 +16,23 @@ powering them. These scripts are responsible for packaging your application, setting up the infrastructure and spinning up necessary services (for example a database). -You can use [project services][project-services] to store credentials to -your infrastructure provider and they will be available during the -deployment. +## How it works + +The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy] +project which is used to simplify the deployment process to Kubernetes by +providing intelligent `build`, `deploy`, and `destroy` commands which you can +use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish), +which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) +to do some of the work, plus some of GitLab's own tools to package it all up. For +your convenience, a [Docker image][kube-image] is also provided. + +You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md) +to store credentials to your infrastructure provider and they will be available +during the deployment. + +## Quick start + +We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com. ## Supported templates @@ -22,20 +41,27 @@ The list of supported auto deploy templates is available in the ## Configuration +>**Note:** +In order to understand why the following steps are required, read the +[how it works](#how-it-works) section. + +To configure Autodeploy, you will need to: + 1. Enable a deployment [project service][project-services] to store your -credentials. For example, if you want to deploy to OpenShift you have to -enable [Kubernetes service][kubernetes-service]. -1. Configure GitLab Runner to use Docker or Kubernetes executor with -[privileged mode enabled][docker-in-docker]. + credentials. For example, if you want to deploy to OpenShift you have to + enable [Kubernetes service][kubernetes-service]. +1. Configure GitLab Runner to use the + [Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with + [privileged mode enabled][docker-in-docker]. 1. Navigate to the "Project" tab and click "Set up auto deploy" button. ![Auto deploy button](img/auto_deploy_button.png) 1. Select a template. ![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png) 1. Commit your changes and create a merge request. 1. Test your deployment configuration using a [Review App][review-app] that was -created automatically for you. + created automatically for you. -## Private Project Support +## Private project support > Experimental support [introduced][mr-2] in GitLab 9.1. @@ -43,7 +69,7 @@ When a project has been marked as private, GitLab's [Container Registry][contain After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release. -## PostgreSQL Database Support +## PostgreSQL database support > Experimental support [introduced][mr-8] in GitLab 9.1. @@ -51,25 +77,13 @@ In order to support applications that require a database, [PostgreSQL][postgresq PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`. -### PostgreSQL Variables +The following PostgreSQL variables are supported: 1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL 1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL 1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL 1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL -[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 -[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2 -[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8 -[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html -[project-services]: ../../user/project/integrations/project_services.md -[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy -[kubernetes-service]: ../../user/project/integrations/kubernetes.md -[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor -[review-app]: ../review_apps/index.md -[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html -[postgresql]: https://www.postgresql.org/ - ## Auto Monitoring > Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438). @@ -94,3 +108,18 @@ If you have installed GitLab using a different method: 1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster 1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml). 1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`. + +[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 +[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2 +[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8 +[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html +[project-services]: ../../user/project/integrations/project_services.md +[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy +[kubernetes-service]: ../../user/project/integrations/kubernetes.md +[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[review-app]: ../review_apps/index.md +[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry" +[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project" +[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html +[postgresql]: https://www.postgresql.org/ + diff --git a/doc/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md new file mode 100644 index 00000000000..f76c2a2cf31 --- /dev/null +++ b/doc/ci/autodeploy/quick_start_guide.md @@ -0,0 +1,95 @@ +# Auto Deploy: quick start guide + +This is a step-by-step guide to deploying a project hosted on GitLab.com to Google Cloud, using Auto Deploy. + +We made a minimal [Ruby application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an example for this guide. It contains two files: + +* `server.rb` - our application. It will start an HTTP server on port 5000 and render “Hello, world!†+* `Dockerfile` - to build our app into a container image. It will use a ruby base image and run `server.rb` + +## Fork sample project on GitLab.com + +Let’s start by forking our sample application. Go to [the project page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` button. Soon you should have a project under your namespace with the necessary files. + +## Setup your own cluster on Google Container Engine + +If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. + +Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. + +## Connect to Kubernetes cluster + +You need to have the Google Cloud SDK installed. e.g. +On OSX, install [homebrew](https://brew.sh): + +1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask` +2. Install Google Cloud SDK: `brew cask install google-cloud-sdk` +3. Add `kubectl`: `gcloud components install kubectl` +4. Log in: `gcloud auth login` + +Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`. + +![connect to cluster](img/guide_connect_cluster.png) + +## Copy credentials to GitLab.com project + +Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token. + +![connect to cluster](img/guide_secret.png) + +You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project `Settings` > `Integrations` > `Project services` > `Kubernetes`. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you. + +![connect to cluster](img/guide_integration.png) + +For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. + +## Expose the application to the internet + +In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address. + +### Set up Ingress controller + +You’ll need to make sure you have an ingress controller. If you don’t have one, do: + +```sh +brew install kubernetes-helm +helm init +helm install --name ruby-app stable/nginx-ingress +``` + +This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that. + +### Point DNS at Cluster IP + +Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running: + +```sh +kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' +``` + +Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>` pointing to the external IP address you found above. + +Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP. + +## Setup Auto Deploy + +Visit the home page of your GitLab.com project and press "Set up Auto Deploy" button. + +![auto deploy button](img/auto_deploy_btn.png) + +You will be redirected to the "New file" page where you can apply one of the Auto Deploy templates. Select "Kubernetes" to apply the template, then in the file, replace `domain.example.com` with your domain name and make any other adjustments you need. + +![auto deploy template](img/auto_deploy_dropdown.png) + +Change the target branch to `master`, and submit your changes. This should create +a new pipeline with several jobs. If you made only the domain name change, the +pipeline will have three jobs: `build`, `staging`, and `production`. + +The `build` job will create a Docker image with your new change and push it to +the GitLab Container Registry. The `staging` job will deploy this image on your +cluster. Once the deploy job succeeds you should be able to see your application by +visiting the Kubernetes dashboard. Select the namespace of your project, which +will look like `ruby-autodeploy-23`, but with a unique ID for your project, and +your app will be listed as "staging" under the "Deployment" tab. + +Once its ready - just visit http://minimal-ruby-app-staging.yourdomain.com to see “Hello, world!†diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 28b27921f8b..cbf06afa294 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -274,9 +274,7 @@ session - and even a multiplexer like `screen` or `tmux`! >**Note:** Container-based deployments often lack basic tools (like an editor), and may be stopped or restarted at any time. If this happens, you will lose all your -changes! Treat this as a debugging tool, not a comprehensive online IDE. You -can use [Koding](../administration/integration/koding.md) for online -development. +changes! Treat this as a debugging tool, not a comprehensive online IDE. --- diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 4ccf1b56771..f5d3b524d6e 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -107,9 +107,26 @@ To lock/unlock a Runner: 1. Check the **Lock to current projects** option 1. Click **Save changes** for the changes to take effect +## Assigning a Runner to another project + +If you are Master on a project where a specific Runner is assigned to, and the +Runner is not [locked only to that project](#locking-a-specific-runner-from-being-enabled-for-other-projects), +you can enable the Runner also on any other project where you have Master permissions. + +To enable/disable a Runner in your project: + +1. Visit your project's **Settings âž” Pipelines** +1. Find the Runner you wish to enable/disable +1. Click **Enable for this project** or **Disable for this project** + +> **Note**: +Consider that if you don't lock your specific Runner to a specific project, any +user with Master role in you project can assign your runner to another arbitrary +project without requiring your authorization, so use it with caution. + ## Protected Runners ->**Notes:** +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13194) in GitLab 10.0. diff --git a/doc/integration/README.md b/doc/integration/README.md index d70b9a7f54b..09d96bdd338 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -13,7 +13,6 @@ Bitbucket.org account - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker -- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration - [LDAP](ldap.md) Set up sign in via LDAP - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 6c11f46a70a..0e20b8096e9 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -224,3 +224,21 @@ By default Sign In is enabled via all the OAuth Providers that have been configu In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. ![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) + + +## Keep OmniAuth user profiles up to date + +You can enable profile syncing from selected OmniAuth providers and for all or for specific user information. + + ```ruby + gitlab_rails['sync_profile_from_provider'] = ['twitter', 'google_oauth2'] + gitlab_rails['sync_profile_attributes'] = ['name', 'email', 'location'] + ``` + + **For installations from source** + + ```yaml + omniauth: + sync_profile_from_provider: ['twitter', 'google_oauth2'] + sync_profile_claims_from_provider: ['email', 'location'] + ```
\ No newline at end of file diff --git a/doc/user/discussions/img/automatically_resolve_outdated_discussions.png b/doc/user/discussions/img/automatically_resolve_outdated_discussions.png Binary files differnew file mode 100644 index 00000000000..9a798ddd178 --- /dev/null +++ b/doc/user/discussions/img/automatically_resolve_outdated_discussions.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 8b1d299484c..efea99eb120 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -116,6 +116,23 @@ are resolved. ![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png) +### Automatically resolve merge request diff discussions when they become outdated + +> [Introduced][ce-14053] in GitLab 10.0. + +You can automatically resolve merge request diff discussions on lines modified +with a new push. + +Navigate to your project's settings page, select the **Automatically resolve +merge request diffs discussions on lines changed with a push** check box and hit +**Save** for the changes to take effect. + +![Automatically resolve merge request diff discussions when they become outdated](img/automatically_resolve_outdated_discussions.png) + +From now on, any discussions on a diff will be resolved by default if a push +makes that diff section outdated. Discussions on lines that don't change and +top-level resolvable discussions are not automatically resolved. + ## Threaded discussions > [Introduced][ce-7527] in GitLab 9.1. @@ -141,6 +158,7 @@ comments in greater detail. [ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527 [ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180 [ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266 +[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/doc/user/permissions.md b/doc/user/permissions.md index dcf210e1085..bd0a58c4cca 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -21,16 +21,16 @@ The following table depicts the various user permission levels in a project. | Action | Guest | Reporter | Developer | Master | Owner | |---------------------------------------|---------|------------|-------------|----------|--------| -| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | -| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ | -| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ | -| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| See a list of jobs | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | -| See a job log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | -| Download and browse job artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ | -| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ | -| Pull project code | | ✓ | ✓ | ✓ | ✓ | -| Download project | | ✓ | ✓ | ✓ | ✓ | +| Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ | +| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | +| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | +| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | +| View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | +| Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ | +| Download project | [^1] | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | @@ -71,8 +71,8 @@ The following table depicts the various user permission levels in a project. | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | -| Force push to protected branches [^3] | | | | | | -| Remove protected branches [^3] | | | | | | +| Force push to protected branches [^4] | | | | | | +| Remove protected branches [^4] | | | | | | | Remove pages | | | | | ✓ | ## Project features permissions @@ -215,13 +215,13 @@ users: | Run CI job | | ✓ | ✓ | ✓ | | Clone source and LFS from current project | | ✓ | ✓ | ✓ | | Clone source and LFS from public projects | | ✓ | ✓ | ✓ | -| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ | -| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] | +| Clone source and LFS from internal projects | | ✓ [^5] | ✓ [^5] | ✓ | +| Clone source and LFS from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | | Push source and LFS | | | | | | Pull container images from current project | | ✓ | ✓ | ✓ | | Pull container images from public projects | | ✓ | ✓ | ✓ | -| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ | -| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] | +| Pull container images from internal projects| | ✓ [^5] | ✓ [^5] | ✓ | +| Pull container images from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | | Push container images to current project | | ✓ | ✓ | ✓ | | Push container images to other projects | | | | | @@ -243,12 +243,11 @@ with the permissions described on the documentation on [auditor users permission Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) only. ----- - -[^1]: Guest users can only view the confidential issues they created themselves -[^2]: If **Public pipelines** is enabled in **Project Settings > Pipelines** -[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner -[^4]: Only if user is not external one. -[^5]: Only if user is a member of the project. +[^1]: On public and internal projects, all users are able to perform this action. +[^2]: Guest users can only view the confidential issues they created themselves +[^3]: If **Public pipelines** is enabled in **Project Settings > Pipelines** +[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner +[^5]: Only if user is not external one. +[^6]: Only if user is a member of the project. [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [new-mod]: project/new_ci_build_permissions_model.md diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md new file mode 100644 index 00000000000..cabd0eef8d6 --- /dev/null +++ b/doc/user/project/import/cvs.md @@ -0,0 +1,68 @@ +# Migrating from CVS + +[CVS](https://savannah.nongnu.org/projects/cvs) is an old centralized version +control system similar to [SVN](svn.md). + +## CVS vs Git + +The following list illustrates the main differences between CVS and Git: + +- **Git is distributed.** On the other hand, CVS is centralized using a client-server + architecture. This translates to Git having a more flexible workflow since + your working area is a copy of the entire repository. This decreases the + overhead when switching branches or merging for example, since you don't have + to communicate with a remote server. +- **Atomic operations.** In Git all operations are + [atomic](https://en.wikipedia.org/wiki/Atomic_commit), either they succeed as + whole, or they fail without any changes. In CVS, commits (and other operations) + are not atomic. If an operation on the repository is interrupted in the middle, + the repository can be left in an inconsistent state. +- **Storage method.** Changes in CVS are per file (changeset), while in Git + a committed file(s) is stored in its entirety (snapshot). That means that's + very easy in Git to revert or undo a whole change. +- **Revision IDs.** The fact that in CVS changes are per files, the revision ID + is depicted by version numbers, for example `1.4` reflects how many time a + given file has been changed. In Git, each version of a project as a whole + (each commit) has its unique name given by SHA-1. +- **Merge tracking.** Git uses a commit-before-merge approach rather than + merge-before-commit (or update-then-commit) like CVS. If while you were + preparing to create a new commit (new revision) somebody created a + new commit on the same branch and pushed to the central repository, CVS would + force you to first update your working directory and resolve conflicts before + allowing you to commit. This is not the case with Git. You first commit, save + your state in version control, then you merge the other developer's changes. + You can also ask the other developer to do the merge and resolve any conflicts + themselves. +- **Signed commits.** Git supports signing your commits with GPG for additional + security and verification that the commit indeed came from its original author. + GitLab can [integrate with GPG](../repository/gpg_signed_commits/index.md) + and show whether a signed commit is correctly verified. + +_Some of the items above were taken from this great +[Stack Overflow post](https://stackoverflow.com/a/824241/974710). For a more +complete list of differences, consult the +Wikipedia article on [comparing the different version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software)._ + +## Why migrate + +CVS is old with no new release since 2008. Git provides more tools to work +with (`git bisect` for one) which makes for a more productive workflow. +Migrating to Git/GitLab there is: + +- **Shorter learning curve**, Git has a big community and a vast number of + tutorials to get you started (see our [Git topic](../../../topics/git/index.md)). +- **Integration with modern tools**, migrating to Git and GitLab you can have + an open source end-to-end software development platform with built-in version + control, issue tracking, code review, CI/CD, and more. +- **Support for many network protocols**. Git supports SSH, HTTP/HTTPS and rsync + among others, whereas CVS supports only SSH and its own insecure pserver + protocol with no user authentication. + +## How to migrate + +Here's a few links to get you started with the migration: + +- [Migrate using the `cvs-fast-export` tool](http://www.catb.org/~esr/reposurgeon/dvcs-migration-guide.html) ([_source code_](https://gitlab.com/esr/cvs-fast-export)) +- [Stack Overflow post on importing the CVS repo](https://stackoverflow.com/a/11490134/974710) +- [Convert a CVS repository to Git](http://www.techrepublic.com/blog/linux-and-open-source/convert-cvs-repositories-to-git/) +- [Man page of the `git-cvsimport` tool](https://www.kernel.org/pub/software/scm/git/docs/git-cvsimport.html) diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 67e856a97cd..8da6e2a8207 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -1,13 +1,15 @@ # Migrating projects to a GitLab instance 1. [From Bitbucket.org](bitbucket.md) +1. [From ClearCase](clearcase.md) +1. [From CVS](cvs.md) +1. [From FogBugz](fogbugz.md) 1. [From GitHub.com of GitHub Enterprise](github.md) 1. [From GitLab.com](gitlab_com.md) -1. [From FogBugz](fogbugz.md) 1. [From Gitea](gitea.md) -1. [From SVN](svn.md) -1. [From ClearCase](clearcase.md) 1. [From Perforce](perforce.md) +1. [From SVN](svn.md) +1. [From TFS](tfs.md) In addition to the specific migration documentation above, you can import any Git repository via HTTP from the New Project page. Be aware that if the diff --git a/doc/user/project/import/tfs.md b/doc/user/project/import/tfs.md new file mode 100644 index 00000000000..8727c2ff6c3 --- /dev/null +++ b/doc/user/project/import/tfs.md @@ -0,0 +1,42 @@ +# Migrating from TFS + +[TFS](https://www.visualstudio.com/tfs/) is a set of tools developed by Microsoft +which also includes a centralized version control system (TFVC) similar to Git. + +In this document, we emphasize on the TFVC to Git migration. + +## TFVC vs Git + +The following list illustrates the main differences between TFVC and Git: + +- **Git is distributed** whereas TFVC is centralized using a client-server + architecture. This translates to Git having a more flexible workflow since + your working area is a copy of the entire repository. This decreases the + overhead when switching branches or merging for example, since you don't have + to communicate with a remote server. +- **Storage method.** Changes in CVS are per file (changeset), while in Git + a committed file(s) is stored in its entirety (snapshot). That means that's + very easy in Git to revert or undo a whole change. + +_Check also Microsoft's documentation on the +[comparison of Git and TFVC](https://www.visualstudio.com/en-us/docs/tfvc/comparison-git-tfvc) +and the Wikipedia article on +[comparing the different version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software)._ + +## Why migrate + +Migrating to Git/GitLab there is: + +- **No licensing costs**, Git is GPL while TFVC is proprietary. +- **Shorter learning curve**, Git has a big community and a vast number of + tutorials to get you started (see our [Git topic](../../../topics/git/index.md)). +- **Integration with modern tools**, migrating to Git and GitLab you can have + an open source end-to-end software development platform with built-in version + control, issue tracking, code review, CI/CD, and more. + +## How to migrate + +The best option to migrate from TFVC to Git is to use the +[`git-tfs`](https://github.com/git-tfs/git-tfs) tool. A specific guide for the +migration exists: +[Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md). diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 41a96246292..d6b3d59d407 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -67,8 +67,6 @@ website with GitLab Pages **Other features:** - [Cycle Analytics](cycle_analytics.md): Review your development lifecycle -- [Koding integration](koding.md) (not available on GitLab.com): Integrate -with Koding to have access to a web terminal right from the GitLab UI - [Syntax highlighting](highlighting.md): An alternative to customize your code blocks, overriding GitLab's default choice of language diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index eb8cd821ddc..9f0308d8111 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -23,4 +23,4 @@ Prometheus server up and running. You have two options here: In order to isolate and only display relevant metrics for a given environment however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments). -If you are using [GitLab Auto-Deploy][autodeploy] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. +If you are using [GitLab Auto-Deploy][../../../ci/autodeploy/index.md] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png Binary files differindex 82e0dd8e85e..355be80ecb6 100755..100644 --- a/doc/user/project/issues/img/confidential_issues_system_notes.png +++ b/doc/user/project/issues/img/confidential_issues_system_notes.png diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md index 455e2ee47b4..86e06a39e59 100644 --- a/doc/user/project/koding.md +++ b/doc/user/project/koding.md @@ -1,6 +1,9 @@ # Koding integration -> [Introduced][ce-5909] in GitLab 8.11. +>**Notes:** +- **As of GitLab 10.0, the Koding integration is deprecated and will be removed + in a future version.** +- [Introduced][ce-5909] in GitLab 8.11. This document will guide you through using Koding integration on GitLab in detail. For configuring and installing please follow the diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 3ff5a08d72c..dbc1305101f 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -66,10 +66,30 @@ in the pipelines settings page. ## Visibility of pipelines -For public and internal projects, the pipelines page can be accessed by -anyone and those logged in respectively. If you wish to hide it so that only -the members of the project or group have access to it, uncheck the **Public -pipelines** checkbox and save the changes. +Access to pipelines and job details (including output of logs and artifacts) +is checked against your current user access level and the **Public pipelines** +project setting. + +If **Public pipelines** is enabled (default): + +- for **public** projects, anyone can view the pipelines and access the job details + (output logs and artifacts) +- for **internal** projects, any logged in user can view the pipelines + and access the job details + (output logs and artifacts) +- for **private** projects, any member (guest or higher) can view the pipelines + and access the job details + (output logs and artifacts) + +If **Public pipelines** is disabled: + +- for **public** projects, anyone can view the pipelines, but only members + (reporter or higher) can access the job details (output logs and artifacts) +- for **internal** projects, any logged in user can view the pipelines, + but only members (reporter or higher) can access the job details (output logs + and artifacts) +- for **private** projects, only members (reporter or higher) + can view the pipelines and access the job details (output logs and artifacts) ## Auto-cancel pending pipelines diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png Binary files differindex 33936a7d6d7..088ecfa6d89 100644 --- a/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png +++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png Binary files differindex 22565cf7c7e..4e3392406b1 100644 --- a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png +++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png Binary files differindex 1778b2ddf2b..766970dee81 100644 --- a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png +++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index ff419d714f9..20aadb8f7ff 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -22,14 +22,25 @@ GitLab uses its own keyring to verify the GPG signature. It does not access any public key server. In order to have a commit verified on GitLab the corresponding public key needs -to be uploaded to GitLab. For a signature to be verified two prerequisites need +to be uploaded to GitLab. For a signature to be verified three conditions need to be met: 1. The public key needs to be added your GitLab account 1. One of the emails in the GPG key matches your **primary** email +1. The committer's email matches the verified email from the gpg key ## Generating a GPG key +>**Notes:** +- If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in + the following commands. +- If Git is using `gpg` and you get errors like `secret key not available` or + `gpg: signing failed: secret key not available`, run the following command to + change to `gpg2`: + ``` + git config --global gpg.program gpg2 + ``` + If you don't already have a GPG key, the following steps will help you get started: diff --git a/doc/user/search/img/issue_search_by_term.png b/doc/user/search/img/issue_search_by_term.png Binary files differnew file mode 100644 index 00000000000..3cefa3adb8b --- /dev/null +++ b/doc/user/search/img/issue_search_by_term.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index f5c7ce49e8e..21e96d8b11c 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -40,6 +40,20 @@ The same process is valid for merge requests. Navigate to your project's **Merge and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. +### Searching for specific terms + +You can filter issues and merge requests by specific terms included in titles or descriptions. + +* Syntax + * Searches look for all the words in a query, in any order. E.g.: searching + issues for `display bug` will return all issues matching both those words, in any order. + * To find the exact term, use double quotes: `"display bug"` +* Limitation + * For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching + issues for `included in titles` is same as `included titles` + +![filter issues by specific terms](img/issue_search_by_term.png) + ### Issues and merge requests per group Similar to **Issues and merge requests per project**, you can also search for issues diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 8fb2ac34c32..962e39dde9a 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -36,13 +36,13 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I should see project "Community" home page' do - page.within '.breadcrumbs .title' do + page.within '.breadcrumbs .breadcrumb-item-text' do expect(page).to have_content 'Community' end end step 'I should see project "Internal" home page' do - page.within '.breadcrumbs .title' do + page.within '.breadcrumbs .breadcrumb-item-text' do expect(page).to have_content 'Internal' end end diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 100e674abed..9ce86ca45d0 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -18,7 +18,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I should see project "Community" home page' do Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") - page.within '.breadcrumbs .title' do + page.within '.breadcrumbs .breadcrumb-item-text' do expect(page).to have_content 'Community' end end diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb new file mode 100644 index 00000000000..3cd5f4ce497 --- /dev/null +++ b/features/support/gitaly.rb @@ -0,0 +1,3 @@ +Spinach.hooks.before_scenario do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 94df543853b..1405a5d0f0e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -108,6 +108,7 @@ module API mount ::API::Internal mount ::API::Issues mount ::API::Jobs + mount ::API::JobArtifacts mount ::API::Keys mount ::API::Labels mount ::API::Lint diff --git a/lib/api/branches.rb b/lib/api/branches.rb index a989394ad91..642c1140fcc 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -24,17 +24,22 @@ module API present paginate(branches), with: Entities::RepoBranch, project: user_project end - desc 'Get a single branch' do - success Entities::RepoBranch - end - params do - requires :branch, type: String, desc: 'The name of the branch' - end - get ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do - branch = user_project.repository.find_branch(params[:branch]) - not_found!("Branch") unless branch + resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do + desc 'Get a single branch' do + success Entities::RepoBranch + end + params do + requires :branch, type: String, desc: 'The name of the branch' + end + head do + user_project.repository.branch_exists?(params[:branch]) ? status(204) : status(404) + end + get do + branch = user_project.repository.find_branch(params[:branch]) + not_found!('Branch') unless branch - present branch, with: Entities::RepoBranch, project: user_project + present branch, with: Entities::RepoBranch, project: user_project + end end # Note: This API will be deprecated in favor of the protected branches API. diff --git a/lib/api/commits.rb b/lib/api/commits.rb index ea78737288a..4b8d248f5f7 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -104,7 +104,7 @@ module API not_found! 'Commit' unless commit - commit.raw_diffs.to_a + present commit.raw_diffs.to_a, with: Entities::RepoDiff end desc "Get a commit's comments" do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 0092cc14e74..1d224d7bc21 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -119,6 +119,7 @@ module API expose :archived?, as: :archived expose :visibility expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } + expose :resolve_outdated_diff_discussions expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible @@ -290,10 +291,11 @@ module API end class RepoDiff < Grape::Entity - expose :old_path, :new_path, :a_mode, :b_mode, :diff + expose :old_path, :new_path, :a_mode, :b_mode expose :new_file?, as: :new_file expose :renamed_file?, as: :renamed_file expose :deleted_file?, as: :deleted_file + expose :json_safe_diff, as: :diff end class ProtectedRefAccess < Grape::Entity @@ -491,6 +493,10 @@ module API expose :user, using: Entities::UserPublic end + class GPGKey < Grape::Entity + expose :id, :key, :created_at + end + class Note < Grape::Entity # Only Issue and MergeRequest have iid NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3d377fdb9eb..e646c63467a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -128,6 +128,10 @@ module API merge_request end + def find_build!(id) + user_project.builds.find(id.to_i) + end + def authenticate! unauthorized! unless current_user && can?(initial_current_user, :access_api) end @@ -160,6 +164,14 @@ module API authorize! :admin_project, user_project end + def authorize_read_builds! + authorize! :read_build, user_project + end + + def authorize_update_builds! + authorize! :update_build, user_project + end + def require_gitlab_workhorse! unless env['HTTP_GITLAB_WORKHORSE'].present? forbidden!('Request should be executed via GitLab Workhorse') @@ -210,7 +222,7 @@ module API def bad_request!(attribute) message = ["400 (Bad request)"] - message << "\"" + attribute.to_s + "\" not given" + message << "\"" + attribute.to_s + "\" not given" if attribute render_api_error!(message.join(' '), 400) end @@ -432,6 +444,10 @@ module API header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) end + def send_artifacts_entry(build, entry) + header(*Gitlab::Workhorse.send_artifacts_entry(build, entry)) + end + # The Grape Error Middleware only has access to env but no params. We workaround this by # defining a method that returns the right value. def define_params_for_grape_middleware diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index f57ff0f2632..4c0db4d42b1 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -46,6 +46,15 @@ module API ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) end + def redis_ping + result = Gitlab::Redis::SharedState.with { |redis| redis.ping } + + result == 'PONG' + rescue => e + Rails.logger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}") + false + end + private def set_project diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 622bd9650e4..c0fef56378f 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -88,7 +88,8 @@ module API { api_version: API.version, gitlab_version: Gitlab::VERSION, - gitlab_rev: Gitlab::REVISION + gitlab_rev: Gitlab::REVISION, + redis: redis_ping } end @@ -142,6 +143,14 @@ module API { success: true, recovery_codes: codes } end + post '/pre_receive' do + status 200 + + reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase + + { reference_counter_increased: reference_counter_increased } + end + post "/notify_post_receive" do status 200 diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb new file mode 100644 index 00000000000..2a8fa7659bf --- /dev/null +++ b/lib/api/job_artifacts.rb @@ -0,0 +1,80 @@ +module API + class JobArtifacts < Grape::API + before { authenticate_non_get! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + desc 'Download the artifacts file from a job' do + detail 'This feature was introduced in GitLab 8.10' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the job' + end + get ':id/jobs/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_read_builds! + + builds = user_project.latest_successful_builds_for(params[:ref_name]) + latest_build = builds.find_by!(name: params[:job]) + + present_artifacts!(latest_build.artifacts_file) + end + + desc 'Download the artifacts file from a job' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + get ':id/jobs/:job_id/artifacts' do + authorize_read_builds! + + build = find_build!(params[:job_id]) + + present_artifacts!(build.artifacts_file) + end + + desc 'Download a specific file from artifacts archive' do + detail 'This feature was introduced in GitLab 10.0' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + requires :artifact_path, type: String, desc: 'Artifact path' + end + get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do + authorize_read_builds! + + build = find_build!(params[:job_id]) + not_found! unless build.artifacts? + + path = Gitlab::Ci::Build::Artifacts::Path + .new(params[:artifact_path]) + bad_request! unless path.valid? + + send_artifacts_entry(build, path) + end + + desc 'Keep the artifacts to prevent them from being deleted' do + success Entities::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + post ':id/jobs/:job_id/artifacts/keep' do + authorize_update_builds! + + build = find_build!(params[:job_id]) + authorize!(:update_build, build) + return not_found!(build) unless build.artifacts? + + build.keep_artifacts! + + status 200 + present build, with: Entities::Job + end + end + end +end diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 5bab96398fd..3c1c412ba42 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -66,42 +66,11 @@ module API get ':id/jobs/:job_id' do authorize_read_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) present build, with: Entities::Job end - desc 'Download the artifacts file from a job' do - detail 'This feature was introduced in GitLab 8.5' - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - get ':id/jobs/:job_id/artifacts' do - authorize_read_builds! - - build = get_build!(params[:job_id]) - - present_artifacts!(build.artifacts_file) - end - - desc 'Download the artifacts file from a job' do - detail 'This feature was introduced in GitLab 8.10' - end - params do - requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the job' - end - get ':id/jobs/artifacts/:ref_name/download', - requirements: { ref_name: /.+/ } do - authorize_read_builds! - - builds = user_project.latest_successful_builds_for(params[:ref_name]) - latest_build = builds.find_by!(name: params[:job]) - - present_artifacts!(latest_build.artifacts_file) - end - # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. @@ -112,7 +81,7 @@ module API get ':id/jobs/:job_id/trace' do authorize_read_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" content_type 'text/plain' @@ -131,7 +100,7 @@ module API post ':id/jobs/:job_id/cancel' do authorize_update_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) authorize!(:update_build, build) build.cancel @@ -148,7 +117,7 @@ module API post ':id/jobs/:job_id/retry' do authorize_update_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) authorize!(:update_build, build) return forbidden!('Job is not retryable') unless build.retryable? @@ -166,7 +135,7 @@ module API post ':id/jobs/:job_id/erase' do authorize_update_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) authorize!(:update_build, build) return forbidden!('Job is not erasable!') unless build.erasable? @@ -174,25 +143,6 @@ module API present build, with: Entities::Job end - desc 'Keep the artifacts to prevent them from being deleted' do - success Entities::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - post ':id/jobs/:job_id/artifacts/keep' do - authorize_update_builds! - - build = get_build!(params[:job_id]) - authorize!(:update_build, build) - return not_found!(build) unless build.artifacts? - - build.keep_artifacts! - - status 200 - present build, with: Entities::Job - end - desc 'Trigger a manual job' do success Entities::Job detail 'This feature was added in GitLab 8.11' @@ -203,7 +153,7 @@ module API post ":id/jobs/:job_id/play" do authorize_read_builds! - build = get_build!(params[:job_id]) + build = find_build!(params[:job_id]) authorize!(:update_build, build) bad_request!("Unplayable Job") unless build.playable? @@ -216,14 +166,6 @@ module API end helpers do - def find_build(id) - user_project.builds.find_by(id: id.to_i) - end - - def get_build!(id) - find_build(id) || not_found! - end - def filter_builds(builds, scope) return builds if scope.nil? || scope.empty? @@ -234,14 +176,6 @@ module API builds.where(status: available_statuses && scope) end - - def authorize_read_builds! - authorize! :read_build, user_project - end - - def authorize_update_builds! - authorize! :update_build, user_project - end end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 4845242a173..7dc19788462 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -16,6 +16,7 @@ module API optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled' optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' + optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.' @@ -236,6 +237,7 @@ module API at_least_one_of_ce = [ :jobs_enabled, + :resolve_outdated_diff_discussions, :container_registry_enabled, :default_branch, :description, diff --git a/lib/api/users.rb b/lib/api/users.rb index 96f47bb618a..1825c90a23b 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -233,6 +233,86 @@ module API destroy_conditionally!(key) end + desc 'Add a GPG key to a specified user. Available only for admins.' do + detail 'This feature was added in GitLab 10.0' + success Entities::GPGKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key, type: String, desc: 'The new GPG key' + end + post ':id/gpg_keys' do + authenticated_as_admin! + + user = User.find_by(id: params.delete(:id)) + not_found!('User') unless user + + key = user.gpg_keys.new(declared_params(include_missing: false)) + + if key.save + present key, with: Entities::GPGKey + else + render_validation_error!(key) + end + end + + desc 'Get the GPG keys of a specified user. Available only for admins.' do + detail 'This feature was added in GitLab 10.0' + success Entities::GPGKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/gpg_keys' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + present paginate(user.gpg_keys), with: Entities::GPGKey + end + + desc 'Delete an existing GPG key from a specified user. Available only for admins.' do + detail 'This feature was added in GitLab 10.0' + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key_id, type: Integer, desc: 'The ID of the GPG key' + end + delete ':id/gpg_keys/:key_id' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + key = user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + status 204 + key.destroy + end + + desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do + detail 'This feature was added in GitLab 10.0' + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key_id, type: Integer, desc: 'The ID of the GPG key' + end + post ':id/gpg_keys/:key_id/revoke' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + key = user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + key.revoke + status :accepted + end + desc 'Add an email address to a specified user. Available only for admins.' do success Entities::Email end @@ -492,6 +572,76 @@ module API destroy_conditionally!(key) end + desc "Get the currently authenticated user's GPG keys" do + detail 'This feature was added in GitLab 10.0' + success Entities::GPGKey + end + params do + use :pagination + end + get 'gpg_keys' do + present paginate(current_user.gpg_keys), with: Entities::GPGKey + end + + desc 'Get a single GPG key owned by currently authenticated user' do + detail 'This feature was added in GitLab 10.0' + success Entities::GPGKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the GPG key' + end + get 'gpg_keys/:key_id' do + key = current_user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + present key, with: Entities::GPGKey + end + + desc 'Add a new GPG key to the currently authenticated user' do + detail 'This feature was added in GitLab 10.0' + success Entities::GPGKey + end + params do + requires :key, type: String, desc: 'The new GPG key' + end + post 'gpg_keys' do + key = current_user.gpg_keys.new(declared_params) + + if key.save + present key, with: Entities::GPGKey + else + render_validation_error!(key) + end + end + + desc 'Revoke a GPG key owned by currently authenticated user' do + detail 'This feature was added in GitLab 10.0' + end + params do + requires :key_id, type: Integer, desc: 'The ID of the GPG key' + end + post 'gpg_keys/:key_id/revoke' do + key = current_user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + key.revoke + status :accepted + end + + desc 'Delete a GPG key from the currently authenticated user' do + detail 'This feature was added in GitLab 10.0' + end + params do + requires :key_id, type: Integer, desc: 'The ID of the SSH key' + end + delete 'gpg_keys/:key_id' do + key = current_user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + status 204 + key.destroy + end + desc "Get the currently authenticated user's email addresses" do success Entities::Email end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index a9a35f2a4bd..ac47a713966 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -64,6 +64,7 @@ module API expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace + expose :resolve_outdated_diff_discussions expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 449876c10d9..74df246bdfe 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -18,6 +18,7 @@ module API optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled' optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' + optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' @@ -296,9 +297,9 @@ module API use :optional_params at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, - :shared_runners_enabled, :container_registry_enabled, - :lfs_enabled, :public, :visibility_level, :public_builds, - :request_access_enabled, :only_allow_merge_if_build_succeeds, + :shared_runners_enabled, :resolve_outdated_diff_discussions, + :container_registry_enabled, :lfs_enabled, :public, :visibility_level, + :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds, :only_allow_merge_if_all_discussions_are_resolved, :path, :default_branch end diff --git a/lib/banzai/commit_renderer.rb b/lib/banzai/commit_renderer.rb new file mode 100644 index 00000000000..f5ff95e3eb3 --- /dev/null +++ b/lib/banzai/commit_renderer.rb @@ -0,0 +1,11 @@ +module Banzai + module CommitRenderer + ATTRIBUTES = [:description, :title].freeze + + def self.render(commits, project, user = nil) + obj_renderer = ObjectRenderer.new(project, user) + + ATTRIBUTES.each { |attr| obj_renderer.render(commits, attr) } + end + end +end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 8e7084f2543..47151626208 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -22,40 +22,94 @@ module Banzai result[:toc] = "" headers = Hash.new(0) + header_root = current_header = HeaderNode.new doc.css('h1, h2, h3, h4, h5, h6').each do |node| - text = node.text + if header_content = node.children.first + id = node + .text + .downcase + .gsub(PUNCTUATION_REGEXP, '') # remove punctuation + .tr(' ', '-') # replace spaces with dash + .squeeze('-') # replace multiple dashes with one - id = text.downcase - id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation - id.tr!(' ', '-') # replace spaces with dash - id.squeeze!('-') # replace multiple dashes with one + uniq = headers[id] > 0 ? "-#{headers[id]}" : '' + headers[id] += 1 + href = "#{id}#{uniq}" - uniq = (headers[id] > 0) ? "-#{headers[id]}" : '' - headers[id] += 1 + current_header = HeaderNode.new(node: node, href: href, previous_header: current_header) - if header_content = node.children.first - # namespace detection will be automatically handled via javascript (see issue #22781) - namespace = "user-content-" - href = "#{id}#{uniq}" - push_toc(href, text) - header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href)) + header_content.add_previous_sibling(anchor_tag(href)) end end - result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty? + push_toc(header_root.children, root: true) doc end private - def anchor_tag(id, href) - %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>} + def anchor_tag(href) + %Q{<a id="user-content-#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>} end - def push_toc(href, text) - result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n} + def push_toc(children, root: false) + return if children.empty? + + klass = ' class="section-nav"' if root + + result[:toc] << "<ul#{klass}>" + children.each { |child| push_anchor(child) } + result[:toc] << '</ul>' + end + + def push_anchor(header_node) + result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>} + push_toc(header_node.children) + result[:toc] << '</li>' + end + + class HeaderNode + attr_reader :node, :href, :parent, :children + + def initialize(node: nil, href: nil, previous_header: nil) + @node = node + @href = href + @children = [] + + @parent = find_parent(previous_header) + @parent.children.push(self) if @parent + end + + def level + return 0 unless node + + @level ||= node.name[1].to_i + end + + def text + return '' unless node + + @text ||= node.text + end + + private + + def find_parent(previous_header) + return unless previous_header + + if level == previous_header.level + parent = previous_header.parent + elsif level > previous_header.level + parent = previous_header + else + parent = previous_header + parent = parent.parent while parent.level >= level + end + + parent + end end end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 2196a92474c..e40556e869c 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -38,7 +38,7 @@ module Banzai objects.each_with_index do |object, index| redacted_data = redacted[index] object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend - object.user_visible_reference_count = redacted_data[:visible_reference_count] + object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count) end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 95d82d17658..ceca9296851 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -36,6 +36,10 @@ module Banzai # The context to use is managed by the object and cannot be changed. # Use #render, passing it the field text, if a custom rendering is needed. def self.render_field(object, field) + unless object.respond_to?(:cached_markdown_fields) + return cacheless_render_field(object, field) + end + object.refresh_markdown_cache!(do_update: update_object?(object)) unless object.cached_html_up_to_date?(field) object.cached_html_for(field) diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb index c6fa928d565..823e8e9a9c4 100644 --- a/lib/github/representation/branch.rb +++ b/lib/github/representation/branch.rb @@ -41,7 +41,7 @@ module Github def remove!(name) repository.delete_branch(name) - rescue Rugged::ReferenceError => e + rescue Gitlab::Git::Repository::DeleteBranchError => e Rails.logger.error("#{self.class.name}: Could not remove branch #{name}: #{e}") end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 4714ab18cc1..b4012ebbb99 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -67,10 +67,14 @@ module Gitlab def protection_values protection_options.values end + + def human_access(access) + options_with_owner.key(access) + end end def human_access - Gitlab::Access.options_with_owner.key(access_field) + Gitlab::Access.human_access(access_field) end def owner? diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb index 2e073334abc..22941d48edf 100644 --- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -1,129 +1,129 @@ module Gitlab - module Ci::Build::Artifacts - class Metadata - ## - # Class that represents an entry (path and metadata) to a file or - # directory in GitLab CI Build Artifacts binary file / archive - # - # This is IO-operations safe class, that does similar job to - # Ruby's Pathname but without the risk of accessing filesystem. - # - # This class is working only with UTF-8 encoded paths. - # - class Entry - attr_reader :path, :entries - attr_accessor :name - - def initialize(path, entries) - @path = path.dup.force_encoding('UTF-8') - @entries = entries - - if path.include?("\0") - raise ArgumentError, 'Path contains zero byte character!' - end + module Ci + module Build + module Artifacts + class Metadata + ## + # Class that represents an entry (path and metadata) to a file or + # directory in GitLab CI Build Artifacts binary file / archive + # + # This is IO-operations safe class, that does similar job to + # Ruby's Pathname but without the risk of accessing filesystem. + # + # This class is working only with UTF-8 encoded paths. + # + class Entry + attr_reader :entries + attr_accessor :name + + def initialize(path, entries) + @entries = entries + @path = Artifacts::Path.new(path) + end + + delegate :empty?, to: :children + + def directory? + blank_node? || @path.directory? + end + + def file? + !directory? + end + + def blob + return unless file? + + @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil) + end + + def has_parent? + nodes > 0 + end + + def parent + return nil unless has_parent? + self.class.new(@path.to_s.chomp(basename), @entries) + end + + def basename + (directory? && !blank_node?) ? name + '/' : name + end + + def name + @name || @path.name + end + + def children + return [] unless directory? + return @children if @children + + child_pattern = %r{^#{Regexp.escape(@path.to_s)}[^/]+/?$} + @children = select_entries { |path| path =~ child_pattern } + end + + def directories(opts = {}) + return [] unless directory? + dirs = children.select(&:directory?) + return dirs unless has_parent? && opts[:parent] + + dotted_parent = parent + dotted_parent.name = '..' + dirs.prepend(dotted_parent) + end + + def files + return [] unless directory? + children.select(&:file?) + end + + def metadata + @entries[@path.to_s] || {} + end + + def nodes + @path.nodes + (file? ? 1 : 0) + end + + def blank_node? + @path.to_s.empty? # "" is considered to be './' + end + + def exists? + blank_node? || @entries.include?(@path.to_s) + end + + def total_size + descendant_pattern = %r{^#{Regexp.escape(@path.to_s)}} + entries.sum do |path, entry| + (entry[:size] if path =~ descendant_pattern).to_i + end + end + + def path + @path.to_s + end + + def to_s + @path.to_s + end + + def ==(other) + path == other.path && @entries == other.entries + end + + def inspect + "#{self.class.name}: #{self}" + end - unless path.valid_encoding? - raise ArgumentError, 'Path contains non-UTF-8 byte sequence!' + private + + def select_entries + selected = @entries.select { |path, _metadata| yield path } + selected.map { |path, _metadata| self.class.new(path, @entries) } + end end end - - delegate :empty?, to: :children - - def directory? - blank_node? || @path.end_with?('/') - end - - def file? - !directory? - end - - def blob - return unless file? - - @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil) - end - - def has_parent? - nodes > 0 - end - - def parent - return nil unless has_parent? - self.class.new(@path.chomp(basename), @entries) - end - - def basename - (directory? && !blank_node?) ? name + '/' : name - end - - def name - @name || @path.split('/').last.to_s - end - - def children - return [] unless directory? - return @children if @children - - child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$} - @children = select_entries { |path| path =~ child_pattern } - end - - def directories(opts = {}) - return [] unless directory? - dirs = children.select(&:directory?) - return dirs unless has_parent? && opts[:parent] - - dotted_parent = parent - dotted_parent.name = '..' - dirs.prepend(dotted_parent) - end - - def files - return [] unless directory? - children.select(&:file?) - end - - def metadata - @entries[@path] || {} - end - - def nodes - @path.count('/') + (file? ? 1 : 0) - end - - def blank_node? - @path.empty? # "" is considered to be './' - end - - def exists? - blank_node? || @entries.include?(@path) - end - - def total_size - descendant_pattern = %r{^#{Regexp.escape(@path)}} - entries.sum do |path, entry| - (entry[:size] if path =~ descendant_pattern).to_i - end - end - - def to_s - @path - end - - def ==(other) - @path == other.path && @entries == other.entries - end - - def inspect - "#{self.class.name}: #{@path}" - end - - private - - def select_entries - selected = @entries.select { |path, _metadata| yield path } - selected.map { |path, _metadata| self.class.new(path, @entries) } - end end end end diff --git a/lib/gitlab/ci/build/artifacts/path.rb b/lib/gitlab/ci/build/artifacts/path.rb new file mode 100644 index 00000000000..9cd9b36c5f8 --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/path.rb @@ -0,0 +1,51 @@ +module Gitlab + module Ci + module Build + module Artifacts + class Path + def initialize(path) + @path = path.dup.force_encoding('UTF-8') + end + + def valid? + nonzero? && utf8? + end + + def directory? + @path.end_with?('/') + end + + def name + @path.split('/').last.to_s + end + + def nodes + @path.count('/') + end + + def to_s + @path.tap do |path| + unless nonzero? + raise ArgumentError, 'Path contains zero byte character!' + end + + unless utf8? + raise ArgumentError, 'Path contains non-UTF-8 byte sequence!' + end + end + end + + private + + def nonzero? + @path.exclude?("\0") + end + + def utf8? + @path.valid_encoding? + end + end + end + end + end +end diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 8ddc91e341d..7b3483a7f96 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -22,10 +22,10 @@ module Gitlab # return message if message type is binary detect = CharlockHolmes::EncodingDetector.detect(message) - return message.force_encoding("BINARY") if detect && detect[:type] == :binary + return message.force_encoding("BINARY") if detect_binary?(message, detect) - # force detected encoding if we have sufficient confidence. if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD + # force detected encoding if we have sufficient confidence. message.force_encoding(detect[:encoding]) end @@ -36,6 +36,19 @@ module Gitlab "--broken encoding: #{encoding}" end + def detect_binary?(data, detect = nil) + detect ||= CharlockHolmes::EncodingDetector.detect(data) + detect && detect[:type] == :binary && detect[:confidence] == 100 + end + + def detect_libgit2_binary?(data) + # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks + # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15), + # which is what we use below to keep a consistent behavior. + detect = CharlockHolmes::EncodingDetector.new(8000).detect(data) + detect && detect[:type] == :binary + end + def encode_utf8(message) detect = CharlockHolmes::EncodingDetector.detect(message) if detect && detect[:encoding] diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 7780f4e4d4f..8d96826f6ee 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -42,14 +42,6 @@ module Gitlab end end - def binary?(data) - # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks - # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15), - # which is what we use below to keep a consistent behavior. - detect = CharlockHolmes::EncodingDetector.new(8000).detect(data) - detect && detect[:type] == :binary - end - # Returns an array of Blob instances, specified in blob_references as # [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the # full blob contents are returned. If blob_size_limit >= 0 then each blob will @@ -65,6 +57,10 @@ module Gitlab end end + def binary?(data) + EncodingHelper.detect_libgit2_binary?(data) + end + private # Recursive search of blob id by path diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index ce3d65062e8..a23c8cf0dd1 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -116,6 +116,15 @@ module Gitlab filtered_opts end + + # Return a binary diff message like: + # + # "Binary files a/file/path and b/file/path differ\n" + # This is used when we detect that a diff is binary + # using CharlockHolmes when Rugged treats it as text. + def binary_message(old_path, new_path) + "Binary files #{old_path} and #{new_path} differ\n" + end end def initialize(raw_diff, expanded: true) @@ -190,6 +199,13 @@ module Gitlab @collapsed = true end + def json_safe_diff + return @diff unless detect_binary?(@diff) + + # the diff is binary, let's make a message for it + Diff.binary_message(@old_path, @new_path) + end + private def init_from_rugged(rugged) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index c67f7724307..efa13590a2c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -18,6 +18,7 @@ module Gitlab InvalidBlobName = Class.new(StandardError) InvalidRef = Class.new(StandardError) GitError = Class.new(StandardError) + DeleteBranchError = Class.new(StandardError) class << self # Unlike `new`, `create` takes the storage path, not the storage name @@ -134,15 +135,19 @@ module Gitlab # This is to work around a bug in libgit2 that causes in-memory refs to # be stale/invalid when packed-refs is changed. # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333 - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/474 def find_branch(name, force_reload = false) - reload_rugged if force_reload + gitaly_migrate(:find_branch) do |is_enabled| + if is_enabled + gitaly_ref_client.find_branch(name) + else + reload_rugged if force_reload - rugged_ref = rugged.branches[name] - if rugged_ref - target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) - Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) + rugged_ref = rugged.branches[name] + if rugged_ref + target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) + end + end end end @@ -605,11 +610,60 @@ module Gitlab # TODO: implement this method end + def add_branch(branch_name, committer:, target:) + target_object = Ref.dereference_object(lookup(target)) + raise InvalidRef.new("target not found: #{target}") unless target_object + + OperationService.new(committer, self).add_branch(branch_name, target_object.oid) + find_branch(branch_name) + rescue Rugged::ReferenceError => ex + raise InvalidRef, ex + end + + def add_tag(tag_name, committer:, target:, message: nil) + target_object = Ref.dereference_object(lookup(target)) + raise InvalidRef.new("target not found: #{target}") unless target_object + + committer = Committer.from_user(committer) if committer.is_a?(User) + + options = nil # Use nil, not the empty hash. Rugged cares about this. + if message + options = { + message: message, + tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name) + } + end + + OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options) + + find_tag(tag_name) + rescue Rugged::ReferenceError => ex + raise InvalidRef, ex + end + + def rm_branch(branch_name, committer:) + OperationService.new(committer, self).rm_branch(find_branch(branch_name)) + end + + def rm_tag(tag_name, committer:) + OperationService.new(committer, self).rm_tag(find_tag(tag_name)) + end + + def find_tag(name) + tags.find { |tag| tag.name == name } + end + # Delete the specified branch from the repository - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/476 def delete_branch(branch_name) - rugged.branches.delete(branch_name) + gitaly_migrate(:delete_branch) do |is_enabled| + if is_enabled + gitaly_ref_client.delete_branch(branch_name) + else + rugged.branches.delete(branch_name) + end + end + rescue Rugged::ReferenceError, CommandError => e + raise DeleteBranchError, e end def delete_refs(*ref_names) @@ -634,15 +688,14 @@ module Gitlab # Examples: # create_branch("feature") # create_branch("other-feature", "master") - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/476 def create_branch(ref, start_point = "HEAD") - rugged_ref = rugged.branches.create(ref, start_point) - target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) - Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) - rescue Rugged::ReferenceError => e - raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ - raise InvalidRef.new("Invalid reference #{start_point}") + gitaly_migrate(:create_branch) do |is_enabled| + if is_enabled + gitaly_ref_client.create_branch(ref, start_point) + else + rugged_create_branch(ref, start_point) + end + end end # Delete the specified remote from this repository. @@ -1179,6 +1232,15 @@ module Gitlab false end + def rugged_create_branch(ref, start_point) + rugged_ref = rugged.branches.create(ref, start_point) + target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) + Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) + rescue Rugged::ReferenceError => e + raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ + raise InvalidRef.new("Invalid reference #{start_point}") + end + def gitaly_copy_gitattributes(revision) gitaly_repository_client.apply_gitattributes(revision) end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index b54962a4456..5cf336af3c6 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -5,7 +5,7 @@ module Gitlab class Tree include Gitlab::EncodingHelper - attr_accessor :id, :root_id, :name, :path, :type, + attr_accessor :id, :root_id, :name, :path, :flat_path, :type, :mode, :commit_id, :submodule_url class << self @@ -19,8 +19,7 @@ module Gitlab Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled| if is_enabled - client = Gitlab::GitalyClient::CommitService.new(repository) - client.tree_entries(repository, sha, path) + repository.gitaly_commit_client.tree_entries(repository, sha, path) else tree_entries_from_rugged(repository, sha, path) end @@ -88,7 +87,7 @@ module Gitlab end def initialize(options) - %w(id root_id name path type mode commit_id).each do |key| + %w(id root_id name path flat_path type mode commit_id).each do |key| self.send("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end end @@ -101,6 +100,10 @@ module Gitlab encode! @path end + def flat_path + encode! @flat_path + end + def dir? type == :tree end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 21a32a7e0db..0825a3a7694 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -88,14 +88,14 @@ module Gitlab response.flat_map do |message| message.entries.map do |gitaly_tree_entry| - entry_path = gitaly_tree_entry.path.dup Gitlab::Git::Tree.new( id: gitaly_tree_entry.oid, root_id: gitaly_tree_entry.root_oid, type: gitaly_tree_entry.type.downcase, mode: gitaly_tree_entry.mode.to_s(8), - name: File.basename(entry_path), - path: entry_path, + name: File.basename(gitaly_tree_entry.path), + path: GitalyClient.encode(gitaly_tree_entry.path), + flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path), commit_id: gitaly_tree_entry.commit_oid ) end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 8c0008c6971..8ef873d5848 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -78,6 +78,54 @@ module Gitlab raise ArgumentError, e.message end + def find_branch(branch_name) + request = Gitaly::FindBranchRequest.new( + repository: @gitaly_repo, + name: GitalyClient.encode(branch_name) + ) + + response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request) + branch = response.branch + return unless branch + + target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) + Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit) + end + + def create_branch(ref, start_point) + request = Gitaly::CreateBranchRequest.new( + repository: @gitaly_repo, + name: GitalyClient.encode(ref), + start_point: GitalyClient.encode(start_point) + ) + + response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request) + + case response.status + when :OK + branch = response.branch + target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) + Gitlab::Git::Branch.new(@repository, branch.name, branch.target_commit.id, target_commit) + when :ERR_INVALID + invalid_ref!("Invalid ref name") + when :ERR_EXISTS + invalid_ref!("Branch #{ref} already exists") + when :ERR_INVALID_START_POINT + invalid_ref!("Invalid reference #{start_point}") + else + raise "Unknown response status: #{response.status}" + end + end + + def delete_branch(branch_name) + request = Gitaly::DeleteBranchRequest.new( + repository: @gitaly_repo, + name: GitalyClient.encode(branch_name) + ) + + GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request) + end + private def consume_refs_response(response) @@ -149,6 +197,10 @@ module Gitlab Gitlab::Git::Commit.decorate(@repository, hash) end + + def invalid_ref!(message) + raise Gitlab::Git::Repository::InvalidRef.new(message) + end end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 373062b354b..b8c07460ebb 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -166,7 +166,7 @@ module Gitlab def remove_branch(name) project.repository.delete_branch(name) - rescue Rugged::ReferenceError + rescue Gitlab::Git::Repository::DeleteBranchFailed errors << { type: :remove_branch, name: name } end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 45e9f9d65ae..025f826e65f 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -39,7 +39,7 @@ module Gitlab fingerprints = CurrentKeyChain.fingerprints_from_key(key) GPGME::Key.find(:public, fingerprints).flat_map do |raw_key| - raw_key.uids.map { |uid| { name: uid.name, email: uid.email } } + raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } } end end end diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 606c7576f70..86bd9f5b125 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -1,17 +1,12 @@ module Gitlab module Gpg class Commit - def self.for_commit(commit) - new(commit.project, commit.sha) - end - - def initialize(project, sha) - @project = project - @sha = sha + def initialize(commit) + @commit = commit @signature_text, @signed_text = begin - Rugged::Commit.extract_signature(project.repository.rugged, sha) + Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha) rescue Rugged::OdbError nil end @@ -26,7 +21,7 @@ module Gitlab return @signature if @signature - cached_signature = GpgSignature.find_by(commit_sha: @sha) + cached_signature = GpgSignature.find_by(commit_sha: @commit.sha) return @signature = cached_signature if cached_signature.present? @signature = create_cached_signature! @@ -73,20 +68,31 @@ module Gitlab def attributes(gpg_key) user_infos = user_infos(gpg_key) + verification_status = verification_status(gpg_key) { - commit_sha: @sha, - project: @project, + commit_sha: @commit.sha, + project: @commit.project, gpg_key: gpg_key, gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, gpg_key_user_name: user_infos[:name], gpg_key_user_email: user_infos[:email], - valid_signature: gpg_signature_valid_signature_value(gpg_key) + verification_status: verification_status } end - def gpg_signature_valid_signature_value(gpg_key) - !!(gpg_key && gpg_key.verified? && verified_signature.valid?) + def verification_status(gpg_key) + return :unknown_key unless gpg_key + return :unverified_key unless gpg_key.verified? + return :unverified unless verified_signature.valid? + + if gpg_key.verified_and_belongs_to_email?(@commit.committer_email) + :verified + elsif gpg_key.user.all_emails.include?(@commit.committer_email) + :same_user_different_email + else + :other_user + end end def user_infos(gpg_key) diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index a525ee7a9ee..e085eab26c9 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -8,7 +8,7 @@ module Gitlab def run GpgSignature .select(:id, :commit_sha, :project_id) - .where('gpg_key_id IS NULL OR valid_signature = ?', false) + .where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified]) .where(gpg_key_primary_keyid: @gpg_key.primary_keyid) .find_each { |sig| sig.gpg_commit.update_signature!(sig) } end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 78795dd3d92..ec73846d844 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -116,6 +116,7 @@ excluded_attributes: statuses: - :trace - :token + - :when push_event_payload: - :event_id diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index cbc8d170936..3bc095a99a9 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -9,6 +9,8 @@ module Gitlab @user = user @shared = shared @project = project + @project_id = project.id + @saved = true end def restore @@ -22,8 +24,10 @@ module Gitlab @project_members = @tree_hash.delete('project_members') - ActiveRecord::Base.no_touching do - create_relations + ActiveRecord::Base.uncached do + ActiveRecord::Base.no_touching do + create_relations + end end rescue => e @shared.error(e) @@ -48,21 +52,24 @@ module Gitlab # the configuration yaml file too. # Finally, it updates each attribute in the newly imported project. def create_relations - saved = [] default_relation_list.each do |relation| - next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present? + if relation.is_a?(Hash) + create_sub_relations(relation, @tree_hash) + elsif @tree_hash[relation.to_s].present? + save_relation_hash(@tree_hash[relation.to_s], relation) + end + end - create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash) + @saved + end - relation_key = relation.is_a?(Hash) ? relation.keys.first : relation - relation_hash_list = @tree_hash[relation_key.to_s] + def save_relation_hash(relation_hash_batch, relation_key) + relation_hash = create_relation(relation_key, relation_hash_batch) - next unless relation_hash_list + @saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash) - relation_hash = create_relation(relation_key, relation_hash_list) - saved << restored_project.append_or_update_attribute(relation_key, relation_hash) - end - saved.all? + # Restore the project again, extra query that skips holding the AR objects in memory + @restored_project = Project.find(@project_id) end def default_relation_list @@ -93,20 +100,42 @@ module Gitlab # issue, finds any subrelations such as notes, creates them and assign them back to the hash # # Recursively calls this method if the sub-relation is a hash containing more sub-relations - def create_sub_relations(relation, tree_hash) + def create_sub_relations(relation, tree_hash, save: true) relation_key = relation.keys.first.to_s return if tree_hash[relation_key].blank? - [tree_hash[relation_key]].flatten.each do |relation_item| - relation.values.flatten.each do |sub_relation| - # We just use author to get the user ID, do not attempt to create an instance. - next if sub_relation == :author + tree_array = [tree_hash[relation_key]].flatten + + # Avoid keeping a possible heavy object in memory once we are done with it + while relation_item = tree_array.shift + # The transaction at this level is less speedy than one single transaction + # But we can't have it in the upper level or GC won't get rid of the AR objects + # after we save the batch. + Project.transaction do + process_sub_relation(relation, relation_item) + + # For every subrelation that hangs from Project, save the associated records alltogether + # This effectively batches all records per subrelation item, only keeping those in memory + # We have to keep in mind that more batch granularity << Memory, but >> Slowness + if save + save_relation_hash([relation_item], relation_key) + tree_hash[relation_key].delete(relation_item) + end + end + end + + tree_hash.delete(relation_key) if save + end + + def process_sub_relation(relation, relation_item) + relation.values.flatten.each do |sub_relation| + # We just use author to get the user ID, do not attempt to create an instance. + next if sub_relation == :author - create_sub_relations(sub_relation, relation_item) if sub_relation.is_a?(Hash) + create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash) - relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) - relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? - end + relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) + relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? end end @@ -121,14 +150,12 @@ module Gitlab end def create_relation(relation, relation_hash_list) - relation_type = relation.to_sym - relation_array = [relation_hash_list].flatten.map do |relation_hash| - Gitlab::ImportExport::RelationFactory.create(relation_sym: relation_type, - relation_hash: parsed_relation_hash(relation_hash, relation_type), + Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, + relation_hash: parsed_relation_hash(relation_hash, relation.to_sym), members_mapper: members_mapper, user: @user, - project: restored_project) + project: @restored_project) end.compact relation_hash_list.is_a?(Array) ? relation_array : relation_array.first diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 5d6de8bc475..9fd0b709ef2 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -16,7 +16,7 @@ module Gitlab error_out(error.message, caller[0].dup) @errors << error.message # Debug: - Rails.logger.error(error.backtrace) + Rails.logger.error(error.backtrace.join("\n")) end private diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb new file mode 100644 index 00000000000..505810964bc --- /dev/null +++ b/lib/gitlab/issuables_count_for_state.rb @@ -0,0 +1,50 @@ +module Gitlab + # Class for counting and caching the number of issuables per state. + class IssuablesCountForState + # The name of the RequestStore cache key. + CACHE_KEY = :issuables_count_for_state + + # The state values that can be safely casted to a Symbol. + STATES = %w[opened closed merged all].freeze + + # finder - The finder class to use for retrieving the issuables. + def initialize(finder) + @finder = finder + @cache = + if RequestStore.active? + RequestStore[CACHE_KEY] ||= initialize_cache + else + initialize_cache + end + end + + def for_state_or_opened(state = nil) + self[state || :opened] + end + + # Returns the count for the given state. + # + # state - The name of the state as either a String or a Symbol. + # + # Returns an Integer. + def [](state) + state = state.to_sym if cast_state_to_symbol?(state) + + cache_for_finder[state] || 0 + end + + private + + def cache_for_finder + @cache[@finder] + end + + def cast_state_to_symbol?(state) + state.is_a?(String) && STATES.include?(state) + end + + def initialize_cache + Hash.new { |hash, finder| hash[finder] = finder.count_by_state } + end + end +end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 39180dc17d9..3bf27b37ae6 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -36,7 +36,7 @@ module Gitlab end def find_by_email - ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_email? + ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_attribute?(:email) end def update_user_attributes @@ -60,7 +60,7 @@ module Gitlab ldap_config.block_auto_created_users end - def sync_email_from_provider? + def sync_profile_from_provider? true end diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 7d6911a1ab3..1f331b1e91d 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -32,8 +32,21 @@ module Gitlab @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase) end - def has_email? - get_info(:email).present? + def location + location = get_info(:address) + if location.is_a?(Hash) + [location.locality.presence, location.country.presence].compact.join(', ') + else + location + end + end + + def has_attribute?(attribute) + if attribute == :location + get_info(:address).present? + else + get_info(attribute).present? + end end private diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index e8330917e91..7704bf715e4 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(auth_hash) self.auth_hash = auth_hash - update_email + update_profile if sync_profile_from_provider? end def persisted? @@ -184,20 +184,30 @@ module Gitlab } end - def sync_email_from_provider? - auth_hash.provider.to_s == Gitlab.config.omniauth.sync_email_from_provider.to_s + def sync_profile_from_provider? + providers = Gitlab.config.omniauth.sync_profile_from_provider + + if providers.is_a?(Array) + providers.include?(auth_hash.provider) + else + providers + end end - def update_email - if auth_hash.has_email? && sync_email_from_provider? - if persisted? - gl_user.skip_reconfirmation! - gl_user.email = auth_hash.email - end + def update_profile + user_synced_attributes_metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata - gl_user.external_email = true - gl_user.email_provider = auth_hash.provider + UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key| + if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key) + gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend + user_synced_attributes_metadata.set_attribute_synced(key, true) + else + user_synced_attributes_metadata.set_attribute_synced(key, false) + end end + + user_synced_attributes_metadata.provider = auth_hash.provider + gl_user.user_synced_attributes_metadata = user_synced_attributes_metadata end def log diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index 8a7cc690046..0f323a9e8b2 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -40,7 +40,7 @@ module Gitlab end def find_by_email - if auth_hash.has_email? + if auth_hash.has_attribute?(:email) user = ::User.find_by(email: auth_hash.email.downcase) user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) if user user diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb index b42bc67ccfc..7c2d1d8f887 100644 --- a/lib/gitlab/sql/pattern.rb +++ b/lib/gitlab/sql/pattern.rb @@ -4,6 +4,7 @@ module Gitlab extend ActiveSupport::Concern MIN_CHARS_FOR_PARTIAL_MATCHING = 3 + REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/ class_methods do def to_pattern(query) @@ -17,6 +18,28 @@ module Gitlab def partial_matching?(query) query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING end + + def to_fuzzy_arel(column, query) + words = select_fuzzy_words(query) + + matches = words.map { |word| arel_table[column].matches(to_pattern(word)) } + + matches.reduce { |result, match| result.and(match) } + end + + def select_fuzzy_words(query) + quoted_words = query.scan(REGEX_QUOTED_WORD) + + query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') } + + words = query.split(/\s+/) + + quoted_words.map! { |quoted_word| quoted_word[1..-2] } + + words.concat(quoted_words) + + words.select { |word| partial_matching?(word) } + end end end end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index c81dc7e30d0..703adae12cb 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -9,7 +9,7 @@ module Gitlab end def self.valid?(url) - return false unless url + return false unless url.present? Addressable::URI.parse(url.strip) @@ -19,7 +19,12 @@ module Gitlab end def initialize(url, credentials: nil) - @url = Addressable::URI.parse(url.strip) + @url = Addressable::URI.parse(url.to_s.strip) + + %i[user password].each do |symbol| + credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol) + end + @credentials = credentials end @@ -29,13 +34,13 @@ module Gitlab def masked_url url = @url.dup - url.password = "*****" unless url.password.nil? - url.user = "*****" unless url.user.nil? + url.password = "*****" if url.password.present? + url.user = "*****" if url.user.present? url.to_s end def credentials - @credentials ||= { user: @url.user, password: @url.password } + @credentials ||= { user: @url.user.presence, password: @url.password.presence } end def full_url @@ -47,8 +52,10 @@ module Gitlab def generate_full_url return @url unless valid_credentials? @full_url = @url.dup - @full_url.user = credentials[:user] + @full_url.password = credentials[:password] + @full_url.user = credentials[:user] + @full_url end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index e5ad9b5a40c..7a94af2f8f1 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -121,10 +121,10 @@ module Gitlab ] end - def send_artifacts_entry(build, entry) + def send_artifacts_entry(build, path) params = { 'Archive' => build.artifacts_file.path, - 'Entry' => Base64.encode64(entry.path) + 'Entry' => Base64.encode64(path.to_s) } [ diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb index 015c7ed1731..53a47eb0f42 100644 --- a/lib/system_check/app/init_script_up_to_date_check.rb +++ b/lib/system_check/app/init_script_up_to_date_check.rb @@ -7,26 +7,22 @@ module SystemCheck set_skip_reason 'skipped (omnibus-gitlab has no init script)' def skip? - omnibus_gitlab? - end + return true if omnibus_gitlab? - def multi_check - recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab') + unless init_file_exists? + self.skip_reason = "can't check because of previous errors" - unless File.exist?(SCRIPT_PATH) - $stdout.puts "can't check because of previous errors".color(:magenta) - return + true end + end + + def check? + recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab') recipe_content = File.read(recipe_path) script_content = File.read(SCRIPT_PATH) - if recipe_content == script_content - $stdout.puts 'yes'.color(:green) - else - $stdout.puts 'no'.color(:red) - show_error - end + recipe_content == script_content end def show_error @@ -38,6 +34,12 @@ module SystemCheck ) fix_and_rerun end + + private + + def init_file_exists? + File.exist?(SCRIPT_PATH) + end end end end diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index 7f9e2ffffc2..0f5742dd67f 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -62,6 +62,25 @@ module SystemCheck call_or_return(@skip_reason) || 'skipped' end + # Define a reason why we skipped the SystemCheck (during runtime) + # + # This is used when you need dynamic evaluation like when you have + # multiple reasons why a check can fail + # + # @param [String] reason to be displayed + def skip_reason=(reason) + @skip_reason = reason + end + + # Skip reason defined during runtime + # + # This value have precedence over the one defined in the subclass + # + # @return [String] the reason + def skip_reason + @skip_reason + end + # Does the check support automatically repair routine? # # @return [Boolean] whether check implemented `#repair!` method or not diff --git a/lib/system_check/incoming_email/foreman_configured_check.rb b/lib/system_check/incoming_email/foreman_configured_check.rb new file mode 100644 index 00000000000..1db7bf2b782 --- /dev/null +++ b/lib/system_check/incoming_email/foreman_configured_check.rb @@ -0,0 +1,23 @@ +module SystemCheck + module IncomingEmail + class ForemanConfiguredCheck < SystemCheck::BaseCheck + set_name 'Foreman configured correctly?' + + def check? + path = Rails.root.join('Procfile') + + File.exist?(path) && File.read(path) =~ /^mail_room:/ + end + + def show_error + try_fixing_it( + 'Enable mail_room in your Procfile.' + ) + for_more_information( + 'doc/administration/reply_by_email.md' + ) + fix_and_rerun + end + end + end +end diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb new file mode 100644 index 00000000000..dee108d987b --- /dev/null +++ b/lib/system_check/incoming_email/imap_authentication_check.rb @@ -0,0 +1,45 @@ +module SystemCheck + module IncomingEmail + class ImapAuthenticationCheck < SystemCheck::BaseCheck + set_name 'IMAP server credentials are correct?' + + def check? + if mailbox_config + begin + imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) + imap.starttls if config[:start_tls] + imap.login(config[:email], config[:password]) + connected = true + rescue + connected = false + end + end + + connected + end + + def show_error + try_fixing_it( + 'Check that the information in config/gitlab.yml is correct' + ) + for_more_information( + 'doc/administration/reply_by_email.md' + ) + fix_and_rerun + end + + private + + def mailbox_config + return @config if @config + + config_path = Rails.root.join('config', 'mail_room.yml').to_s + erb = ERB.new(File.read(config_path)) + erb.filename = config_path + config_file = YAML.load(erb.result) + + @config = config_file[:mailboxes]&.first + end + end + end +end diff --git a/lib/system_check/incoming_email/initd_configured_check.rb b/lib/system_check/incoming_email/initd_configured_check.rb new file mode 100644 index 00000000000..ea23b8ef49c --- /dev/null +++ b/lib/system_check/incoming_email/initd_configured_check.rb @@ -0,0 +1,32 @@ +module SystemCheck + module IncomingEmail + class InitdConfiguredCheck < SystemCheck::BaseCheck + set_name 'Init.d configured correctly?' + + def skip? + omnibus_gitlab? + end + + def check? + mail_room_configured? + end + + def show_error + try_fixing_it( + 'Enable mail_room in the init.d configuration.' + ) + for_more_information( + 'doc/administration/reply_by_email.md' + ) + fix_and_rerun + end + + private + + def mail_room_configured? + path = '/etc/default/gitlab' + File.exist?(path) && File.read(path).include?('mail_room_enabled=true') + end + end + end +end diff --git a/lib/system_check/incoming_email/mail_room_running_check.rb b/lib/system_check/incoming_email/mail_room_running_check.rb new file mode 100644 index 00000000000..c1807501829 --- /dev/null +++ b/lib/system_check/incoming_email/mail_room_running_check.rb @@ -0,0 +1,43 @@ +module SystemCheck + module IncomingEmail + class MailRoomRunningCheck < SystemCheck::BaseCheck + set_name 'MailRoom running?' + + def skip? + return true if omnibus_gitlab? + + unless mail_room_configured? + self.skip_reason = "can't check because of previous errors" + true + end + end + + def check? + mail_room_running? + end + + def show_error + try_fixing_it( + sudo_gitlab('RAILS_ENV=production bin/mail_room start') + ) + for_more_information( + see_installation_guide_section('Install Init Script'), + 'see log/mail_room.log for possible errors' + ) + fix_and_rerun + end + + private + + def mail_room_configured? + path = '/etc/default/gitlab' + File.exist?(path) && File.read(path).include?('mail_room_enabled=true') + end + + def mail_room_running? + ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww)) + ps_ux.include?("mail_room") + end + end + end +end diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index 6604b1078cf..00221f77cf4 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -23,7 +23,7 @@ module SystemCheck # # @param [BaseCheck] check class def <<(check) - raise ArgumentError unless check < BaseCheck + raise ArgumentError unless check.is_a?(Class) && check < BaseCheck @checks << check end @@ -48,7 +48,7 @@ module SystemCheck # When implements skip method, we run it first, and if true, skip the check if check.can_skip? && check.skip? - $stdout.puts check_klass.skip_reason.color(:magenta) + $stdout.puts check.skip_reason.try(:color, :magenta) || check_klass.skip_reason.color(:magenta) return end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index f7f2fa2f14c..35ba729c156 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -1,5 +1,4 @@ require "gettext_i18n_rails/tasks" -require 'simple_po_parser' namespace :gettext do # Customize list of translatable files @@ -23,6 +22,8 @@ namespace :gettext do desc 'Lint all po files in `locale/' task lint: :environment do + require 'simple_po_parser' + FastGettext.silence_errors files = Dir.glob(Rails.root.join('locale/*/gitlab.po')) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 92a3f503fcb..654f638c454 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -309,133 +309,24 @@ namespace :gitlab do desc "GitLab | Check the configuration of Reply by email" task check: :environment do warn_user_is_not_gitlab - start_checking "Reply by email" if Gitlab.config.incoming_email.enabled - check_imap_authentication + checks = [ + SystemCheck::IncomingEmail::ImapAuthenticationCheck + ] if Rails.env.production? - check_initd_configured_correctly - check_mail_room_running + checks << SystemCheck::IncomingEmail::InitdConfiguredCheck + checks << SystemCheck::IncomingEmail::MailRoomRunningCheck else - check_foreman_configured_correctly + checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck end - else - puts 'Reply by email is disabled in config/gitlab.yml' - end - - finished_checking "Reply by email" - end - - # Checks - ######################## - - def check_initd_configured_correctly - return if omnibus_gitlab? - - print "Init.d configured correctly? ... " - - path = "/etc/default/gitlab" - - if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "yes".color(:green) - else - puts "no".color(:red) - try_fixing_it( - "Enable mail_room in the init.d configuration." - ) - for_more_information( - "doc/administration/reply_by_email.md" - ) - fix_and_rerun - end - end - - def check_foreman_configured_correctly - print "Foreman configured correctly? ... " - path = Rails.root.join("Procfile") - - if File.exist?(path) && File.read(path) =~ /^mail_room:/ - puts "yes".color(:green) + SystemCheck.run('Reply by email', checks) else - puts "no".color(:red) - try_fixing_it( - "Enable mail_room in your Procfile." - ) - for_more_information( - "doc/administration/reply_by_email.md" - ) - fix_and_rerun - end - end - - def check_mail_room_running - return if omnibus_gitlab? - - print "MailRoom running? ... " - - path = "/etc/default/gitlab" - - unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "can't check because of previous errors".color(:magenta) - return - end - - if mail_room_running? - puts "yes".color(:green) - else - puts "no".color(:red) - try_fixing_it( - sudo_gitlab("RAILS_ENV=production bin/mail_room start") - ) - for_more_information( - see_installation_guide_section("Install Init Script"), - "see log/mail_room.log for possible errors" - ) - fix_and_rerun - end - end - - def check_imap_authentication - print "IMAP server credentials are correct? ... " - - config_path = Rails.root.join('config', 'mail_room.yml').to_s - erb = ERB.new(File.read(config_path)) - erb.filename = config_path - config_file = YAML.load(erb.result) - - config = config_file[:mailboxes].first - - if config - begin - imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) - imap.starttls if config[:start_tls] - imap.login(config[:email], config[:password]) - connected = true - rescue - connected = false - end - end - - if connected - puts "yes".color(:green) - else - puts "no".color(:red) - try_fixing_it( - "Check that the information in config/gitlab.yml is correct" - ) - for_more_information( - "doc/administration/reply_by_email.md" - ) - fix_and_rerun + puts 'Reply by email is disabled in config/gitlab.yml' end end - - def mail_room_running? - ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww)) - ps_ux.include?("mail_room") - end end namespace :ldap do diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index d8ab1f253e8..fcd4aa29834 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 10:02-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Bulgarian\n" "Language: bg_BG\n" @@ -57,9 +57,18 @@ msgstr "Ðабор от графики отноÑно непрекъÑнатат msgid "About auto deploy" msgstr "ОтноÑно автоматичното внедрÑване" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Ðктивно" @@ -84,6 +93,12 @@ msgstr "ДобавÑне на нова папка" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Ðрхивиран проект! Хранилището е Ñамо за четене" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Прикачете файл чрез влачене и пуÑкане или %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "Клон" @@ -137,6 +209,9 @@ msgstr "Разглеждане на файловете" msgid "ByAuthor|by" msgstr "от" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð½Ð° непрекъÑната интеграциÑ" @@ -164,6 +239,9 @@ msgstr "СпиÑък Ñ Ð¿Ñ€Ð¾Ð¼ÐµÐ½Ð¸" msgid "Charts" msgstr "Графики" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Подбиране на това подаване" @@ -259,12 +337,18 @@ msgstr "Подадено от" msgid "Compare" msgstr "Сравнение" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "РъководÑтво за ÑътрудничеÑтво" msgid "Contributors" msgstr "Сътрудници" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Копиране на адреÑа в буфера за обмен" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "ВнедрÑване" msgstr[1] "ВнедрÑваниÑ" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "ОпиÑание" @@ -399,6 +486,9 @@ msgstr "Редактиране" msgid "Edit Pipeline Schedule %{id}" msgstr "Редактиране на плана %{id} за Ñхема" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "От Ñъздаването на проблема до внедрÑваРmsgid "From merge request merge until deploy to production" msgstr "От прилагането на заÑвката за Ñливане до внедрÑването в крайната верÑиÑ" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Към Вашето разклонение" msgid "GoToYourFork|Fork" msgstr "Разклонение" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "Ðачало" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "ОÑвежаването започна уÑпешно" @@ -515,14 +617,8 @@ msgstr "ПредÑтавÑме Ви анализа на циклите" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Задачи за поÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð¼ÐµÑец" - -msgid "Jobs for last week" -msgstr "Задачи за поÑледната Ñедмица" - -msgid "Jobs for last year" -msgstr "Задачи за поÑледната година" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Изключено" @@ -530,6 +626,9 @@ msgstr "Изключено" msgid "LFSStatus|Enabled" msgstr "Включено" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "ПоÑÐ»ÐµÐ´Ð½Ð¸Ñ %d ден" @@ -562,20 +661,38 @@ msgstr "ÐапуÑкане на групата" msgid "Leave project" msgstr "ÐапуÑкане на проекта" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Ограничено до показване на най-много %d Ñъбитие" msgstr[1] "Ограничено до показване на най-много %d ÑъбитиÑ" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Медиана" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "добавите SSH ключ" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "УчаÑтие" msgid "NotificationLevel|Watch" msgstr "Ðаблюдение" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Филтър" @@ -686,9 +806,15 @@ msgstr "Отворен" msgid "Options" msgstr "Опции" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "СобÑтвеник" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "Схема" @@ -701,6 +827,9 @@ msgstr "План за Ñхема" msgid "Pipeline Schedules" msgstr "Планове за Ñхема" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "ÐеуÑпешни:" @@ -764,6 +893,15 @@ msgstr "Схеми" msgid "Pipelines charts" msgstr "Графики за Ñхемите" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "вÑички" @@ -776,6 +914,12 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿" msgid "Pipeline|with stages" msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "ИзнаÑÑнето на проекта започна. Ще получ msgid "Project home" msgstr "Ðачална Ñтраница на проекта" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Етап" msgid "ProjectNetworkGraph|Graph" msgstr "Графика" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "ОтмÑна на това подаване" msgid "Revert this merge request" msgstr "ОтмÑна на тази заÑвка за Ñливане" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Запазване на плана за Ñхема" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Изберете целеви клон" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}." @@ -935,14 +1091,23 @@ msgstr "ÐаÑтройка на авт. внедрÑване" msgid "SetPasswordToCloneLink|set a password" msgstr "зададете парола" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Показване на %d Ñъбитие" msgstr[1] "Показване на %d ÑъбитиÑ" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Изходен код" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "ИÑкате ли да видите данните? Помолете аРmsgid "We don't have enough data to show this stage." msgstr "ÐÑма доÑтатъчно данни за този етап." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "ОттеглÑне на заÑвката за доÑтъп" @@ -1284,4 +1452,5 @@ msgstr "извеÑÑ‚Ð¸Ñ Ð¿Ð¾ е-поща" msgid "parent" msgid_plural "parents" msgstr[0] "родител" -msgstr[1] "родители"
\ No newline at end of file +msgstr[1] "родители" + diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index 3cefb26d234..86deb620f0b 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:29-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: German\n" "Language: de_DE\n" @@ -23,20 +23,20 @@ msgstr[1] "" msgid "%s additional commit has been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%s zusätzlicher Commit wurde ausgelassen um Leistungsprobleme zu verhindern." +msgstr[1] "%s zusätzliche Commits wurden ausgelassen um Leistungsprobleme zu verhindern." msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" +msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird den Zugriff beim nächsten Versuch zulassen." msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." -msgstr "" +msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird den Zugriff für %{number_of_seconds} Sekunden blockieren." msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." -msgstr "" +msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird es nicht weiter versuchen. Setze die Speicherinformation nach Behebung des Problems zurück." msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" @@ -44,7 +44,7 @@ msgstr[0] "" msgstr[1] "" msgid "(checkout the %{link} for information on how to install it)." -msgstr "" +msgstr "(beachte die Informationen zur Installation auf %{link})." msgid "1 pipeline" msgid_plural "%d pipelines" @@ -52,12 +52,21 @@ msgstr[0] "" msgstr[1] "" msgid "A collection of graphs regarding Continuous Integration" -msgstr "" +msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration" msgid "About auto deploy" +msgstr "Über automatische Bereitstellung " + +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" msgstr "" msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "Zugriff auf fehlerhafte Speicher wurde vorübergehend deaktiviert, um die Wiederherstellung zu ermöglichen. Für den zukünftigen Zugriff, behebe bitte das Problem und setze danach die Speicherinformationen zurück." + +msgid "Account" msgstr "" msgid "Active" @@ -67,42 +76,105 @@ msgid "Activity" msgstr "" msgid "Add Changelog" -msgstr "" +msgstr "Änderungsliste hinzufügen " msgid "Add Contribution guide" -msgstr "" +msgstr "Mitarbeitsanleitung hinzufügen" msgid "Add License" msgstr "" msgid "Add an SSH key to your profile to pull or push via SSH." -msgstr "" +msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu übertragen (push) oder abzurufen (pull)." msgid "Add new directory" -msgstr "" +msgstr "Erstelle eine neues Verzeichnis" msgid "All" +msgstr "Alle" + +msgid "Appearances" msgstr "" -msgid "Archived project! Repository is read-only" +msgid "Applications" msgstr "" +msgid "Archived project! Repository is read-only" +msgstr "Archiviertes Projekt! Repository ist nicht änderbar." + msgid "Are you sure you want to delete this pipeline schedule?" -msgstr "" +msgstr "Bist Du sicher, dass Du diesen Pipeline-Zeitplan löschen möchtest?" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "Bist Du sicher, dass Du alle Änderungen zurücksetzen willst?" msgid "Are you sure you want to reset registration token?" -msgstr "" +msgstr "Bist Du sicher, dass Du den Registrierungstoken zurücksetzen willst?" msgid "Are you sure you want to reset the health check token?" -msgstr "" +msgstr "Bist Du sicher, dass Du den Systemüberwachungstoken zurücksetzen willst?" msgid "Are you sure?" -msgstr "" +msgstr "Bist Du sicher?" msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "Datei mittels Drag & Drop oder %{upload_link} hinzufügen" + +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" msgstr "" msgid "Branch" @@ -111,31 +183,34 @@ msgstr[0] "" 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 "" +msgstr "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe Deine Änderungen. %{link_to_autodeploy_doc}" msgid "BranchSwitcherPlaceholder|Search branches" -msgstr "" +msgstr "Branches durchsuchen" msgid "BranchSwitcherTitle|Switch branch" -msgstr "" +msgstr "Branch wechseln" msgid "Branches" msgstr "" msgid "Browse Directory" -msgstr "" +msgstr "Verzeichnisse durchsuchen" msgid "Browse File" -msgstr "" +msgstr "Datei durchsuchen" msgid "Browse Files" -msgstr "" +msgstr "Dateien durchsuchen" msgid "Browse files" -msgstr "" +msgstr "Dateien durchsuchen" msgid "ByAuthor|by" -msgstr "Von" +msgstr "von" + +msgid "CI / CD" +msgstr "" msgid "CI configuration" msgstr "" @@ -144,88 +219,91 @@ msgid "Cancel" msgstr "" msgid "Cancel edit" -msgstr "" +msgstr "Bearbeitung abbrechen" msgid "ChangeTypeActionLabel|Pick into branch" -msgstr "" +msgstr "In dem Branch wählen" msgid "ChangeTypeActionLabel|Revert in branch" -msgstr "" +msgstr "Im Branch wiederherstellen" msgid "ChangeTypeAction|Cherry-pick" -msgstr "" +msgstr "Herauspicken" msgid "ChangeTypeAction|Revert" -msgstr "" +msgstr "Wiederherstellen " msgid "Changelog" -msgstr "" +msgstr "Änderungsliste " msgid "Charts" +msgstr "Diagramme" + +msgid "Chat" msgstr "" msgid "Cherry-pick this commit" -msgstr "" +msgstr "Diesen Commit herauspicken " msgid "Cherry-pick this merge request" -msgstr "" +msgstr "Diesen Merge Request herauspicken" msgid "CiStatusLabel|canceled" -msgstr "" +msgstr "abgebrochen" msgid "CiStatusLabel|created" -msgstr "" +msgstr "erstellt" msgid "CiStatusLabel|failed" -msgstr "" +msgstr "fehlgeschlagen" msgid "CiStatusLabel|manual action" -msgstr "" +msgstr "manuelles Eingreifen" msgid "CiStatusLabel|passed" -msgstr "" +msgstr "absolviert" msgid "CiStatusLabel|passed with warnings" -msgstr "" +msgstr "mit Warnungen absolviert" msgid "CiStatusLabel|pending" -msgstr "" +msgstr "ausstehend" msgid "CiStatusLabel|skipped" -msgstr "" +msgstr "übersprungen" msgid "CiStatusLabel|waiting for manual action" -msgstr "" +msgstr "wartet auf manuelles Eingreifen" msgid "CiStatusText|blocked" -msgstr "" +msgstr "blockiert" msgid "CiStatusText|canceled" -msgstr "" +msgstr "abgebrochen" msgid "CiStatusText|created" -msgstr "" +msgstr "erstellt" msgid "CiStatusText|failed" -msgstr "" +msgstr "fehlgeschlagen" msgid "CiStatusText|manual" -msgstr "" +msgstr "manuell" msgid "CiStatusText|passed" -msgstr "" +msgstr "absolviert" msgid "CiStatusText|pending" -msgstr "" +msgstr "ausstehend" msgid "CiStatusText|skipped" -msgstr "" +msgstr "übersprungen" msgid "CiStatus|running" -msgstr "" +msgstr "laufend" msgid "Comments" -msgstr "" +msgstr "Kommentare" msgid "Commit" msgid_plural "Commits" @@ -233,106 +311,112 @@ msgstr[0] "" msgstr[1] "" msgid "Commit duration in minutes for last 30 commits" -msgstr "" +msgstr "Dauer der Commits in Minuten für die letzten 30 Commits" msgid "Commit message" -msgstr "" +msgstr "Commit Nachricht" msgid "CommitBoxTitle|Commit" -msgstr "" +msgstr "Commit" msgid "CommitMessage|Add %{file_name}" -msgstr "" +msgstr "%{file_name} hinzufügen" msgid "Commits" msgstr "" msgid "Commits feed" -msgstr "" +msgstr "Liste der Commits" msgid "Commits|History" -msgstr "" +msgstr "Verlauf" msgid "Committed by" -msgstr "" +msgstr "Committed von" msgid "Compare" +msgstr "Vergleichen" + +msgid "Container Registry" msgstr "" msgid "Contribution guide" -msgstr "" +msgstr "Mitarbeitsanleitung" msgid "Contributors" +msgstr "Mitarbeiter" + +msgid "Copy SSH public key to clipboard" msgstr "" msgid "Copy URL to clipboard" -msgstr "" +msgstr "Kopiere URL in die Zwischenablage" msgid "Copy commit SHA to clipboard" -msgstr "" +msgstr "Kopiere Commit SHA in die Zwischenablage" msgid "Create New Directory" -msgstr "" +msgstr "Erstelle neues Verzeichnis" msgid "Create a new branch" -msgstr "" +msgstr "Erstelle einen neuen Branch" msgid "Create a personal access token on your account to pull or push via %{protocol}." -msgstr "" +msgstr "Erstelle einen persönlichen Zugriffstoken in Deinem Konto um mittels %{protocol} zu übertragen (push) oder abzurufen (pull)." msgid "Create directory" -msgstr "" +msgstr "Erstelle Verzeichnis" msgid "Create empty bare repository" -msgstr "" +msgstr "Erstelle leeres Repository" msgid "Create merge request" -msgstr "" +msgstr "Erstelle Merge Request" msgid "Create new..." -msgstr "" +msgstr "Erstelle neues..." msgid "CreateNewFork|Fork" -msgstr "" +msgstr "Ableger" msgid "CreateTag|Tag" -msgstr "" +msgstr "Tag " msgid "CreateTokenToCloneLink|create a personal access token" -msgstr "" +msgstr "Erstelle einen persönlichen Zugriffstoken" msgid "Cron Timezone" -msgstr "" +msgstr "Cron Zeitzone" msgid "Cron syntax" -msgstr "" +msgstr "Cron Syntax" msgid "Custom notification events" -msgstr "" +msgstr "Individuelle Benachrichtigungsereignisse" msgid "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}." -msgstr "" +msgstr "Individuelle Benachrichtigungsstufen sind identisch mit den Beteiligungsstufen. Mit individuellen Benachrichtigungsstufen erhältst Du ebenfalls Mitteilungen für ausgewählte Ereignisse. Für weitere Informationen lies %{notification_link}. " msgid "Cycle Analytics" -msgstr "" +msgstr "Arbeitsablaufsanalysen" msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." -msgstr "Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht." +msgstr "Arbeitsablaufsanalysen verschaffen einen Überblick, welche Zeit Dein Projekt von der Idee zur Realisierung benötigt." msgid "CycleAnalyticsStage|Code" -msgstr "Code" +msgstr "Entwicklung" msgid "CycleAnalyticsStage|Issue" -msgstr "Issue" +msgstr "Ticket" msgid "CycleAnalyticsStage|Plan" msgstr "Planung" msgid "CycleAnalyticsStage|Production" -msgstr "Produktiv" +msgstr "Produktion" msgid "CycleAnalyticsStage|Review" -msgstr "Review" +msgstr "Überprüfung" msgid "CycleAnalyticsStage|Staging" msgstr "Staging" @@ -341,281 +425,314 @@ msgid "CycleAnalyticsStage|Test" msgstr "Test" msgid "Define a custom pattern with cron syntax" -msgstr "" +msgstr "Erstelle ein individuelles Muster mittels Cron Syntax" msgid "Delete" -msgstr "" +msgstr "Löschen" msgid "Deploy" msgid_plural "Deploys" -msgstr[0] "Deployment" -msgstr[1] "Deployments" +msgstr[0] "Bereitstellung" +msgstr[1] "Bereitstellungen" -msgid "Description" +msgid "Deploy Keys" msgstr "" +msgid "Description" +msgstr "Beschreibung" + msgid "Details" msgstr "" msgid "Directory name" -msgstr "" +msgstr "Verzeichnisname" msgid "Discard changes" -msgstr "" +msgstr "Änderungen verwerfen" msgid "Don't show again" -msgstr "" +msgstr "Nicht erneut anzeigen" msgid "Download" -msgstr "" +msgstr "Herunterladen" msgid "Download tar" -msgstr "" +msgstr "TAR-Datei herunterladen" msgid "Download tar.bz2" -msgstr "" +msgstr "TAR.BZ2-Datei herunterladen" msgid "Download tar.gz" -msgstr "" +msgstr "TAR.GZ-Datei herunterladen" msgid "Download zip" -msgstr "" +msgstr "ZIP-Datei herunterladen" msgid "DownloadArtifacts|Download" -msgstr "" +msgstr "Herunterladen" msgid "DownloadCommit|Email Patches" -msgstr "" +msgstr "E-Mail Patch" msgid "DownloadCommit|Plain Diff" -msgstr "" +msgstr "Unterschiede" msgid "DownloadSource|Download" -msgstr "" +msgstr "Herunterladen" msgid "Edit" -msgstr "" +msgstr "Bearbeiten" msgid "Edit Pipeline Schedule %{id}" +msgstr "Pipeline Zeitplan bearbeiten %{id}" + +msgid "Emails" msgstr "" msgid "EventFilterBy|Filter by all" -msgstr "" +msgstr "Filtere alle" msgid "EventFilterBy|Filter by comments" -msgstr "" +msgstr "Filtere nach Kommentaren" msgid "EventFilterBy|Filter by issue events" -msgstr "" +msgstr "Filtere nach Tickets" msgid "EventFilterBy|Filter by merge events" -msgstr "" +msgstr "Filtere nach Merge Requests" msgid "EventFilterBy|Filter by push events" -msgstr "" +msgstr "Filtere nach Übertragungen" msgid "EventFilterBy|Filter by team" -msgstr "" +msgstr "Filtere nach Teams" msgid "Every day (at 4:00am)" -msgstr "" +msgstr "Täglich (um 4:00 Uhr)" msgid "Every month (on the 1st at 4:00am)" -msgstr "" +msgstr "Monatlich (am Ersten um 4:00 Uhr)" msgid "Every week (Sundays at 4:00am)" -msgstr "" +msgstr "Wöchentlich (Sonntags um 4:00 Uhr)" msgid "Failed to change the owner" -msgstr "" +msgstr "Wechsel des Besitzers fehlgeschlagen" msgid "Failed to remove the pipeline schedule" -msgstr "" +msgstr "Entfernung der Pipelineplanung fehlgeschlagen" msgid "Files" -msgstr "" +msgstr "Dateien" msgid "Filter by commit message" -msgstr "" +msgstr "Filter nach Commit Nachricht" msgid "Find by path" -msgstr "" +msgstr "Finde über den Pfad" msgid "Find file" -msgstr "" +msgstr "Finde Datei" msgid "FirstPushedBy|First" msgstr "Erster" msgid "FirstPushedBy|pushed by" -msgstr "gepusht von" +msgstr "übertragen von" msgid "Fork" msgid_plural "Forks" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Ableger" +msgstr[1] "Ableger" msgid "ForkedFromProjectPath|Forked from" -msgstr "" +msgstr "Ableger von" msgid "From issue creation until deploy to production" -msgstr "Vom Anlegen des Issues bis zum Produktivdeployment" +msgstr "Von der Ticketbeschreibung bis zur Bereitstellung" msgid "From merge request merge until deploy to production" -msgstr "Vom Merge Request bis zum Produktivdeployment" +msgstr "Vom Umsetzen des Merge Request bis zur Bereitstellung auf dem Produktivsystem" -msgid "Git storage health information has been reset" +msgid "GPG Keys" msgstr "" -msgid "GitLab Runner section" +msgid "Geo Nodes" msgstr "" +msgid "Git storage health information has been reset" +msgstr "Informationen über den Speicherzustand von Gitlab wurden zurückgesetzt." + +msgid "GitLab Runner section" +msgstr "GitLab Runner Bereich" + msgid "Go to your fork" -msgstr "" +msgstr "Gehe zu Deinem Ableger" msgid "GoToYourFork|Fork" +msgstr "Ableger" + +msgid "Group overview" msgstr "" msgid "Health Check" -msgstr "" +msgstr "Systemzustand" msgid "Health information can be retrieved from the following endpoints. More information is available" -msgstr "" +msgstr "Informationen über den Systemzustand können von folgenden Endpunkten erhalten werden. Mehr Informationen gibt es" msgid "HealthCheck|Access token is" -msgstr "" +msgstr "Zugriffstoken ist" msgid "HealthCheck|Healthy" -msgstr "" +msgstr "OK" msgid "HealthCheck|No Health Problems Detected" -msgstr "" +msgstr "Keine Probleme erkannt" msgid "HealthCheck|Unhealthy" -msgstr "" +msgstr "Problematisch" msgid "Home" +msgstr "Startseite" + +msgid "Hooks" msgstr "" msgid "Housekeeping successfully started" -msgstr "" +msgstr "Aufräumen erfolgreich gestartet" msgid "Import repository" -msgstr "" +msgstr "Repository importieren" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "Installiere einen Runner der mit GitLab CI kompatibel ist" msgid "Interval Pattern" -msgstr "" +msgstr "Intervallmuster" msgid "Introducing Cycle Analytics" -msgstr "Was sind Cycle Analytics?" +msgstr "Arbeitsablaufsanalysen vorgestellt" msgid "Issue events" -msgstr "" - -msgid "Jobs for last month" -msgstr "" +msgstr "Ticketereignisse" -msgid "Jobs for last week" -msgstr "" - -msgid "Jobs for last year" +msgid "Issues" msgstr "" msgid "LFSStatus|Disabled" -msgstr "" +msgstr "Deaktiviert" msgid "LFSStatus|Enabled" +msgstr "Aktiviert" + +msgid "Labels" msgstr "" msgid "Last %d day" msgid_plural "Last %d days" -msgstr[0] "Letzter %d Tag" +msgstr[0] "Letzten %d Tag" msgstr[1] "Letzten %d Tage" msgid "Last Pipeline" -msgstr "" +msgstr "Letzte Pipeline" msgid "Last Update" -msgstr "" +msgstr "Letzte Aktualisierung" msgid "Last commit" -msgstr "" +msgstr "Letzter Commit" msgid "LastPushEvent|You pushed to" -msgstr "" +msgstr "Du übertrugst an" msgid "LastPushEvent|at" -msgstr "" +msgstr "am" msgid "Learn more in the" -msgstr "" +msgstr "Erfahre mehr in den" msgid "Learn more in the|pipeline schedules documentation" -msgstr "" +msgstr "Pipelineplanungsdokumentation" msgid "Leave group" -msgstr "" +msgstr "Verlasse die Gruppe" msgid "Leave project" +msgstr "Verlasse das Projekt" + +msgid "License" msgstr "" msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" -msgstr[0] "Eingeschränkt auf maximal %d Ereignis" -msgstr[1] "Eingeschränkt auf maximal %d Ereignisse" +msgstr[0] "Limitiere die Anzeige auf höchstens %d Ereignis" +msgstr[1] "Limitiere die Anzeige auf höchstens %d Ereignisse" + +msgid "Locked Files" +msgstr "" msgid "Median" msgstr "" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" +msgstr "Ereignisse zusammenführen" + +msgid "Messages" msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "einen SSH Schlüssel hinzufügst" + +msgid "Monitoring" msgstr "" msgid "More information is available|here" -msgstr "" +msgstr "hier" msgid "New Issue" msgid_plural "New Issues" -msgstr[0] "Neues Issue" -msgstr[1] "Neue Issues" +msgstr[0] "Neues Ticket" +msgstr[1] "Neue Tickets" msgid "New Pipeline Schedule" -msgstr "" +msgstr "Neuer Pipeline Zeitplan" msgid "New branch" -msgstr "" +msgstr "Neuer Branch" msgid "New directory" -msgstr "" +msgstr "Neues Verzeichnis" msgid "New file" -msgstr "" +msgstr "Neue Datei" msgid "New issue" -msgstr "" +msgstr "Neues Ticket" msgid "New merge request" -msgstr "" +msgstr "Neuer Merge Request" msgid "New schedule" -msgstr "" +msgstr "Neuer Zeitplan" msgid "New snippet" -msgstr "" +msgstr "Neuer Schnipsel" msgid "New tag" -msgstr "" +msgstr "Neuer Tag" msgid "No repository" -msgstr "" +msgstr "Kein Repository" msgid "No schedules" -msgstr "" +msgstr "Keine Zeitpläne" msgid "Not available" msgstr "Nicht verfügbar" @@ -624,241 +741,274 @@ msgid "Not enough data" msgstr "Nicht genügend Daten" msgid "Notification events" -msgstr "" +msgstr "Benachrichtigungsereignisse" msgid "NotificationEvent|Close issue" -msgstr "" +msgstr "Ticket abschließen" msgid "NotificationEvent|Close merge request" -msgstr "" +msgstr "Merge Request abschließen" msgid "NotificationEvent|Failed pipeline" -msgstr "" +msgstr "Fehlgeschlagene Pipeline" msgid "NotificationEvent|Merge merge request" -msgstr "" +msgstr "Merge Request umsetzen" msgid "NotificationEvent|New issue" -msgstr "" +msgstr "Neues Ticket" msgid "NotificationEvent|New merge request" -msgstr "" +msgstr "Neuer Merge Request" msgid "NotificationEvent|New note" -msgstr "" +msgstr "Neue Notiz" msgid "NotificationEvent|Reassign issue" -msgstr "" +msgstr "Ticket neu zuweisen" msgid "NotificationEvent|Reassign merge request" -msgstr "" +msgstr "Merge Request neu zuweisen" msgid "NotificationEvent|Reopen issue" -msgstr "" +msgstr "Ticket wieder öffnen" msgid "NotificationEvent|Successful pipeline" -msgstr "" +msgstr "Erfolgreiche Pipeline" msgid "NotificationLevel|Custom" -msgstr "" +msgstr "Individuell" msgid "NotificationLevel|Disabled" -msgstr "" +msgstr "Deaktiviert" msgid "NotificationLevel|Global" -msgstr "" +msgstr "Global" msgid "NotificationLevel|On mention" -msgstr "" +msgstr "Zum Vermerk" msgid "NotificationLevel|Participate" -msgstr "" +msgstr "Teilnehmen" msgid "NotificationLevel|Watch" +msgstr "Beobachten" + +msgid "Notifications" msgstr "" msgid "OfSearchInADropdown|Filter" -msgstr "" +msgstr "Filter" msgid "OpenedNDaysAgo|Opened" -msgstr "Erstellt" +msgstr "Ungelöst" msgid "Options" +msgstr "Optionen" + +msgid "Overview" msgstr "" msgid "Owner" +msgstr "Besitzer" + +msgid "Password" msgstr "" msgid "Pipeline" msgstr "" msgid "Pipeline Health" -msgstr "Pipeline Kennzahlen" +msgstr "Zustand der Pipeline" msgid "Pipeline Schedule" -msgstr "" +msgstr "Zeitplan der Pipeline" msgid "Pipeline Schedules" +msgstr "Zustände der Pipeline" + +msgid "Pipeline quota" msgstr "" msgid "PipelineCharts|Failed:" -msgstr "" +msgstr "Fehlgeschlagen:" msgid "PipelineCharts|Overall statistics" -msgstr "" +msgstr "Gesamte Statisktiken" msgid "PipelineCharts|Success ratio:" -msgstr "" +msgstr "Erfolgsverhältnis:" msgid "PipelineCharts|Successful:" -msgstr "" +msgstr "Erfolgreich:" msgid "PipelineCharts|Total:" -msgstr "" +msgstr "Insgesamt:" msgid "PipelineSchedules|Activated" -msgstr "" +msgstr "Aktiviert" msgid "PipelineSchedules|Active" -msgstr "" +msgstr "Aktiv" msgid "PipelineSchedules|All" -msgstr "" +msgstr "Alle" msgid "PipelineSchedules|Inactive" -msgstr "" +msgstr "Inaktiv" msgid "PipelineSchedules|Input variable key" -msgstr "" +msgstr "Schlüssel der Eingangsvariable" msgid "PipelineSchedules|Input variable value" -msgstr "" +msgstr "Wert der Eingangsvariable" msgid "PipelineSchedules|Next Run" -msgstr "" +msgstr "Nächste Durchführung" msgid "PipelineSchedules|None" -msgstr "" +msgstr "Nichts" msgid "PipelineSchedules|Provide a short description for this pipeline" -msgstr "" +msgstr "Beschreibe diese Pipeline" msgid "PipelineSchedules|Remove variable row" -msgstr "" +msgstr "Entferne Variablenreihe" msgid "PipelineSchedules|Take ownership" -msgstr "" +msgstr "Eigentümer werden" msgid "PipelineSchedules|Target" -msgstr "" +msgstr "Ziel" msgid "PipelineSchedules|Variables" -msgstr "" +msgstr "Variablen" msgid "PipelineSheduleIntervalPattern|Custom" -msgstr "" +msgstr "Individuell" msgid "Pipelines" msgstr "" msgid "Pipelines charts" +msgstr "Pipelinediagramme" + +msgid "Pipelines for last month" msgstr "" -msgid "Pipeline|all" +msgid "Pipelines for last week" msgstr "" -msgid "Pipeline|success" +msgid "Pipelines for last year" msgstr "" +msgid "Pipeline|all" +msgstr "Alle" + +msgid "Pipeline|success" +msgstr "Erfolg" + msgid "Pipeline|with stage" -msgstr "" +msgstr "mit Stage" msgid "Pipeline|with stages" +msgstr "mit Stages" + +msgid "Preferences" msgstr "" -msgid "Project" +msgid "Profile Settings" msgstr "" +msgid "Project" +msgstr "Projekt" + msgid "Project '%{project_name}' queued for deletion." -msgstr "" +msgstr "Das Projekt '%{project_name}' wurde zur Löschung eingeplant." msgid "Project '%{project_name}' was successfully created." -msgstr "" +msgstr "Das Projekt '%{project_name}' wurde erfolgreich erstellt." msgid "Project '%{project_name}' was successfully updated." -msgstr "" +msgstr "Das Projekt '%{project_name}' wurde erfolgreich aktualisiert." msgid "Project '%{project_name}' will be deleted." -msgstr "" +msgstr "Das Projekt '%{project_name}' wird gelöscht." msgid "Project access must be granted explicitly to each user." -msgstr "" +msgstr "Jedem Nutzer muss explizit der Zugriff auf das Projekt gewährt werden." msgid "Project details" -msgstr "" +msgstr "Projektdetails" msgid "Project export could not be deleted." -msgstr "" +msgstr "Der Export des Projekts konnte nich gelöscht werden." msgid "Project export has been deleted." -msgstr "" +msgstr "Der Export des Projekts wurde gelöscht." msgid "Project export link has expired. Please generate a new export from your project settings." -msgstr "" +msgstr "Der Link für den Export des Projektes ist abgelaufen. Bitte generiere einen neuen Export in den Projekteinstellungen." msgid "Project export started. A download link will be sent by email." -msgstr "" +msgstr "Export des Projektes gestartet. Ein Link zum herunterladen wir Dir per E-Mail zugesandt." msgid "Project home" +msgstr "Startseite des Projektes" + +msgid "Project overview" msgstr "" msgid "ProjectActivityRSS|Subscribe" -msgstr "" +msgstr "Abonnieren" msgid "ProjectFeature|Disabled" -msgstr "" +msgstr "Dekativiert" msgid "ProjectFeature|Everyone with access" -msgstr "" +msgstr "Jeder mit Zugriff" msgid "ProjectFeature|Only team members" -msgstr "" +msgstr "Nur Teammitglieder" msgid "ProjectFileTree|Name" -msgstr "" +msgstr "Name" msgid "ProjectLastActivity|Never" -msgstr "" +msgstr "Niemals" msgid "ProjectLifecycle|Stage" -msgstr "Phase" +msgstr "Stage" msgid "ProjectNetworkGraph|Graph" +msgstr "Diagramm" + +msgid "Push Rules" msgstr "" msgid "Push events" -msgstr "" +msgstr "Übertragungsereignisse" msgid "Read more" -msgstr "Mehr" +msgstr "Mehr lesen" msgid "Readme" -msgstr "" +msgstr "Lies mich" msgid "RefSwitcher|Branches" -msgstr "" +msgstr "Branches" msgid "RefSwitcher|Tags" -msgstr "" +msgstr "Tags" msgid "Related Commits" msgstr "Zugehörige Commits" msgid "Related Deployed Jobs" -msgstr "Zugehörige Deploymentjobs" +msgstr "Zugehörige ausgelieferte Jobs" msgid "Related Issues" -msgstr "Zugehörige Issues" +msgstr "Zugehörige Tickets" msgid "Related Jobs" msgstr "Zugehörige Jobs" @@ -867,72 +1017,81 @@ msgid "Related Merge Requests" msgstr "Zugehörige Merge Requests" msgid "Related Merged Requests" -msgstr "Zugehörige abgeschlossene Merge Requests" +msgstr "Zugehörige umgesetzte Merge Requests" msgid "Remind later" -msgstr "" +msgstr "Später erinnern" msgid "Remove project" -msgstr "" +msgstr "Projekt entfernen" msgid "Repository" msgstr "" msgid "Request Access" -msgstr "" +msgstr "Anfrage auf Zugriff" msgid "Reset git storage health information" -msgstr "" +msgstr "Informationen über Speicherzustand zurücksetzen" msgid "Reset health check access token" -msgstr "" +msgstr "Zugriffstoken für Systemzustand zurücksetzen" msgid "Reset runners registration token" -msgstr "" +msgstr "Registrierungstoken für Runner zurücksetzen" msgid "Revert this commit" -msgstr "" +msgstr "Commit zurücksetzen" msgid "Revert this merge request" +msgstr "Merge Request zurücksetzen" + +msgid "SSH Keys" msgstr "" msgid "Save pipeline schedule" -msgstr "" +msgstr "Zeitplan der Pipeline speichern" msgid "Schedule a new pipeline" -msgstr "" +msgstr "Plane eine neue Pipeline" msgid "Scheduling Pipelines" -msgstr "" +msgstr "Pipelines planen" msgid "Search branches and tags" -msgstr "" +msgstr "Suche nach Branches und Tags" msgid "Select Archive Format" -msgstr "" +msgstr "Archivierungsformat auswählen" msgid "Select a timezone" -msgstr "" +msgstr "Zeitzone auswählen" msgid "Select existing branch" -msgstr "" +msgstr "Existierenden Branch auswählen" msgid "Select target branch" +msgstr "Zielbranch auswählen" + +msgid "Service Templates" msgstr "" msgid "Set a password on your account to pull or push via %{protocol}." -msgstr "" +msgstr "Lege ein Passwort für dein Konto fest, um mittels %{protocol} zu übertragen (push) oder abzurufen (pull)." msgid "Set up CI" -msgstr "" +msgstr "CI einrichten" msgid "Set up Koding" -msgstr "" +msgstr "Koding einrichten" msgid "Set up auto deploy" -msgstr "" +msgstr "Automatische Bereitstellung einrichten" msgid "SetPasswordToCloneLink|set a password" +msgstr "ein Passwort festlegst" + +msgid "Settings" msgstr "" msgid "Showing %d event" @@ -940,23 +1099,29 @@ msgid_plural "Showing %d events" msgstr[0] "Zeige %d Ereignis" msgstr[1] "Zeige %d Ereignisse" +msgid "Snippets" +msgstr "" + msgid "Source code" +msgstr "Quellcode" + +msgid "Spam Logs" msgstr "" msgid "Specify the following URL during the Runner setup:" -msgstr "" +msgstr "Lege die folgende URL während des Runner Setups fest:" msgid "StarProject|Star" -msgstr "" +msgstr "Favorisieren" msgid "Start a %{new_merge_request} with these changes" -msgstr "" +msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen" msgid "Start the Runner!" -msgstr "" +msgstr "Starte den Runner!" msgid "Switch branch/tag" -msgstr "" +msgstr "Zu Branch/Tag wechseln" msgid "Tag" msgid_plural "Tags" @@ -967,308 +1132,311 @@ msgid "Tags" msgstr "" msgid "Target Branch" -msgstr "" +msgstr "Zielbranch" msgid "Team" msgstr "" msgid "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." -msgstr "Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt." +msgstr "Die Entwicklungsphase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Du Deinen ersten Merge Request anlegst, werden dessen Daten automatisch ergänzt." msgid "The collection of events added to the data gathered for that stage." msgstr "Ereignisse, die für diese Phase ausgewertet wurden." msgid "The fork relationship has been removed." -msgstr "" +msgstr "Die Beziehung des Ablegers wurde entfernt." msgid "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." -msgstr "Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen." +msgstr "Die Ticketphase stellt die Zeit vom Anlegen eines Tickets bis zum Zuweisen eines Meilensteins oder Hinzufügen zur Aufgabentafel dar. Erstelle einen Ticket, damit dessen Daten hier erscheinen." msgid "The phase of the development lifecycle." -msgstr "Die Phase im Entwicklungsprozess." +msgstr "Die Phase des Entwicklungslebenszyklus." msgid "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." -msgstr "" +msgstr "Die Pipelinezeitpläne starten Pipelines in der Zukunft, wiederholend, für bestimmte Branches oder Tags. Diese geplanten Pipelines haben denselben begrenzten Zugriff auf das Projekt, wie der zugeordnete Nutzer." msgid "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." -msgstr "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen." +msgstr "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Übertragen des ersten Commits dar. Sobald Du den ersten Commit überträgst, werden dessen Daten hier erscheinen." msgid "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." -msgstr "Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier." +msgstr "Die Produktionsphase stellt die Gesamtzeit vom Anlegen eines Tickets bis zur Bereitstellung des Codes auf dem Produktivsystem dar. Sobald Du den vollständigen Entwicklungszyklus, von einer Idee bis zur Fertigstellung, durchlaufen hast, erscheinen die zugehörigen Daten hier." msgid "The project can be accessed by any logged in user." -msgstr "" +msgstr "Auf das Projekt kann jeder angemeldete Nutzer zugreifen." msgid "The project can be accessed without any authentication." -msgstr "" +msgstr "Auf das Projekt kann ohne Authentifizierung zugegriffen werden." msgid "The repository for this project does not exist." -msgstr "" +msgstr "Das Repository für das Projekt existiert nicht." msgid "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." -msgstr "Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt." +msgstr "Die Überprüfungsphase stellt die Zeit vom Anlegen eines Merge Requests bis dessen Umsetzung dar. Sobald Du Deinen ersten Merge Request abschließt, werden dessen Daten hier automatisch angezeigt." msgid "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." -msgstr "Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt." +msgstr "Die Staging-Phase stellt die Zeit zwischen der Umsetzung eines Merge Requests und der Bereitstellung des Codes auf dem Produktivsystem dar. Sobald Du das erste Mal auf das Produktivsystem ausgeliefert hast, werden dessen Daten hier automatisch angezeigt." msgid "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." -msgstr "Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt." +msgstr "Die Testphase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von zugehörigen Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt." msgid "The time taken by each data entry gathered by that stage." -msgstr "Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde." +msgstr "Zeit, die für das jeweilige Ereignis in der Phase ermittelt wurde." msgid "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." msgstr "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6." msgid "There are problems accessing Git storage: " -msgstr "" +msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:" msgid "This means you can not push code until you create an empty repository or import existing one." -msgstr "" +msgstr "Dies bedeutet, dass Du keinen Code übertragen kannst, bevor Du kein leeres Repositorium erstellt oder ein Existierendes importiert hast." msgid "Time before an issue gets scheduled" -msgstr "Zeit bis ein Issue geplant wird" +msgstr "Zeit bis ein Ticket geplant wird" msgid "Time before an issue starts implementation" -msgstr "Zeit bis die Implementierung für ein Issue beginnt" +msgstr "Zeit bis die Implementierung für ein Ticket beginnt" msgid "Time between merge request creation and merge/close" -msgstr "Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests" +msgstr "Zeit zwischen einem Merge Request und dessen Umsetzung / Schließung" msgid "Time until first merge request" msgstr "Zeit bis zum ersten Merge Request" msgid "Timeago|%s days ago" -msgstr "" +msgstr "seit %s Tagen" msgid "Timeago|%s days remaining" -msgstr "" +msgstr "%s Tage verbleibend" msgid "Timeago|%s hours remaining" -msgstr "" +msgstr "%s Stunden verbleibend" msgid "Timeago|%s minutes ago" -msgstr "" +msgstr "seit %s Minuten " msgid "Timeago|%s minutes remaining" -msgstr "" +msgstr "%s Minuten verbleibend" msgid "Timeago|%s months ago" -msgstr "" +msgstr "seit %s Monaten" msgid "Timeago|%s months remaining" -msgstr "" +msgstr "%s Monate verbleibend" msgid "Timeago|%s seconds remaining" -msgstr "" +msgstr "%s Sekunden verbleibend" msgid "Timeago|%s weeks ago" -msgstr "" +msgstr "seit %s Wochen" msgid "Timeago|%s weeks remaining" -msgstr "" +msgstr "%s Wochen verbleibend" msgid "Timeago|%s years ago" -msgstr "" +msgstr "seit %s Jahren" msgid "Timeago|%s years remaining" -msgstr "" +msgstr "%s Jahre verbleibend" msgid "Timeago|1 day remaining" -msgstr "" +msgstr "1 Tag verbleibend" msgid "Timeago|1 hour remaining" -msgstr "" +msgstr "1 Stunde verbleibend" msgid "Timeago|1 minute remaining" -msgstr "" +msgstr "1 Minute verbleibend" msgid "Timeago|1 month remaining" -msgstr "" +msgstr "1 Monat verbleibend" msgid "Timeago|1 week remaining" -msgstr "" +msgstr "1 Woche verbleibend" msgid "Timeago|1 year remaining" -msgstr "" +msgstr "1 Jahr verbleibend" msgid "Timeago|Past due" -msgstr "" +msgstr "Fällig" msgid "Timeago|a day ago" -msgstr "" +msgstr "vor einem Tag" msgid "Timeago|a month ago" -msgstr "" +msgstr "vor einem Monat" msgid "Timeago|a week ago" -msgstr "" +msgstr "vor einer Woche" msgid "Timeago|a while" -msgstr "" +msgstr "eine Weile" msgid "Timeago|a year ago" -msgstr "" +msgstr "vor einem Jahr" msgid "Timeago|about %s hours ago" -msgstr "" +msgstr "vor ungefähr %s Stunden" msgid "Timeago|about a minute ago" -msgstr "" +msgstr "vor ungefähr einer Minute" msgid "Timeago|about an hour ago" -msgstr "" +msgstr "vor ungefähr einer Stunde" msgid "Timeago|in %s days" -msgstr "" +msgstr "in %s Tagen" msgid "Timeago|in %s hours" -msgstr "" +msgstr "in %s Stunden" msgid "Timeago|in %s minutes" -msgstr "" +msgstr "in %s Minuten" msgid "Timeago|in %s months" -msgstr "" +msgstr "in %s Monaten" msgid "Timeago|in %s seconds" -msgstr "" +msgstr "in %s Sekunden" msgid "Timeago|in %s weeks" -msgstr "" +msgstr "in %s Wochen" msgid "Timeago|in %s years" -msgstr "" +msgstr "in %s Jahren" msgid "Timeago|in 1 day" -msgstr "" +msgstr "in 1 Tag" msgid "Timeago|in 1 hour" -msgstr "" +msgstr "in 1 Stunde" msgid "Timeago|in 1 minute" -msgstr "" +msgstr "in 1 Minute" msgid "Timeago|in 1 month" -msgstr "" +msgstr "in 1 Monat" msgid "Timeago|in 1 week" -msgstr "" +msgstr "in 1 Woche" msgid "Timeago|in 1 year" -msgstr "" +msgstr "in 1 Jahr" msgid "Timeago|less than a minute ago" -msgstr "" +msgstr "vor weniger als einer Minute" msgid "Time|hr" msgid_plural "Time|hrs" -msgstr[0] "h" -msgstr[1] "h" +msgstr[0] "Std." +msgstr[1] "Stdn." msgid "Time|min" msgid_plural "Time|mins" -msgstr[0] "min" -msgstr[1] "min" +msgstr[0] "Min." +msgstr[1] "Min." msgid "Time|s" -msgstr "s" +msgstr "Sek." msgid "Total Time" msgstr "Gesamtzeit" msgid "Total test time for all commits/merges" -msgstr "Gesamte Testlaufzeit für alle Commits/Merges" +msgstr "Gesamte Testzeit für alle Commits/Merges" msgid "Unstar" -msgstr "" +msgstr "Entfavorisieren" msgid "Upload New File" -msgstr "" +msgstr "Eine Neue Datei hochladen" msgid "Upload file" -msgstr "" +msgstr "Eine Datei hochladen" msgid "UploadLink|click to upload" -msgstr "" +msgstr "Zum Upload klicken" msgid "Use the following registration token during setup:" -msgstr "" +msgstr "Benutze den folgenden Registrierungstoken während des Setups:" msgid "Use your global notification setting" -msgstr "" +msgstr "Benutze Deine globalen Benachrichtigungseinstellungen" msgid "View open merge request" -msgstr "" +msgstr "Zeige offene Merge Requests." msgid "VisibilityLevel|Internal" -msgstr "" +msgstr "Intern" msgid "VisibilityLevel|Private" -msgstr "" +msgstr "Privat" msgid "VisibilityLevel|Public" -msgstr "" +msgstr "Öffentlich" msgid "VisibilityLevel|Unknown" -msgstr "" +msgstr "Unbekannt" msgid "Want to see the data? Please ask an administrator for access." -msgstr "Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator." +msgstr "Du möchtest diese Daten sehen? Bitte frage einen Administrator nach dem Zugang." msgid "We don't have enough data to show this stage." msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen." -msgid "Withdraw Access Request" +msgid "Wiki" msgstr "" +msgid "Withdraw Access Request" +msgstr "Zugriffsanfrage widerrufen" + msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "" +msgstr "Du bist dabei %{group_name} zu entfernen. Entfernte Gruppen können NICHT wiederhergestellt werden! Bist Du dir WIRKLICH sicher?" msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "" +msgstr "Du bist dabei %{project_name_with_namespace} zu entfernen. Entfernte Projekte können NICHT wiederhergestellt werden! Bist Du dir WIRKLICH sicher?" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" -msgstr "" +msgstr "Du bist dabei, die Beziehung des Ablegers zum Ursprungsprojekt %{forked_from_project}, zu entfernen. Bist Du dir WIRKLICH sicher?" msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" -msgstr "" +msgstr "Du bist dabei %{project_name_with_namespace} einem andere Besitzer zu übergeben. Bist Du dir WIRKLICH sicher?" msgid "You can only add files when you are on a branch" -msgstr "" +msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befindest." msgid "You have reached your project limit" -msgstr "" +msgstr "Du hast die Projektbegrenzung erreicht." msgid "You must sign in to star a project" -msgstr "" +msgstr "Du musst angemeldet sein, um ein Projekt zu favorisieren." msgid "You need permission." -msgstr "Sie benötigen Zugriffsrechte." +msgstr "Du brauchst eine Genehmigung." msgid "You will not get any notifications via email" -msgstr "" +msgstr "Du wirst keine Benachrichtigungen per E-Mail erhalten." msgid "You will only receive notifications for the events you choose" -msgstr "" +msgstr "Du wirst nur Benachrichtigungen für, von Dir ausgewählte, Ereignisse erhalten." msgid "You will only receive notifications for threads you have participated in" -msgstr "" +msgstr "Du wirst nur Benachrichtigungen für Unterhaltungen, an denen Du teilgenommen hast, erhalten." msgid "You will receive notifications for any activity" -msgstr "" +msgstr "Du wirst bei jeder Aktivität Benachrichtigungen erhalten." msgid "You will receive notifications only for comments in which you were @mentioned" -msgstr "" +msgstr "Du wirst nur Benachrichtigungen für Kommentare erhalten, in denen du @erwähnt wurdest." msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account" -msgstr "" +msgstr "Du kannst erst mittels '%{protocol}' übertragen (push) oder abrufen (pull), nachdem Du für dein Konto '%{set_password_link}'." msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" -msgstr "" +msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachdem Du Deinem Konto '%{add_ssh_key_link}'." msgid "Your name" -msgstr "" +msgstr "Dein Name" msgid "day" msgid_plural "days" @@ -1276,12 +1444,13 @@ msgstr[0] "Tag" msgstr[1] "Tage" msgid "new merge request" -msgstr "" +msgstr "Neuer Merge Request" msgid "notification emails" -msgstr "" +msgstr "Benachrichtungsemail" msgid "parent" msgid_plural "parents" -msgstr[0] "" -msgstr[1] ""
\ No newline at end of file +msgstr[0] "Vorgänger" +msgstr[1] "Vorgänger" + diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index 84232be601e..0ac591d4927 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -82,9 +82,6 @@ msgstr "" msgid "Add new directory" msgstr "" -msgid "All" -msgstr "" - msgid "Archived project! Repository is read-only" msgstr "" @@ -225,9 +222,6 @@ msgstr "" msgid "CiStatus|running" msgstr "" -msgid "Comments" -msgstr "" - msgid "Commit" msgid_plural "Commits" msgstr[0] "" @@ -400,24 +394,6 @@ msgstr "" msgid "Edit Pipeline Schedule %{id}" msgstr "" -msgid "EventFilterBy|Filter by all" -msgstr "" - -msgid "EventFilterBy|Filter by comments" -msgstr "" - -msgid "EventFilterBy|Filter by issue events" -msgstr "" - -msgid "EventFilterBy|Filter by merge events" -msgstr "" - -msgid "EventFilterBy|Filter by push events" -msgstr "" - -msgid "EventFilterBy|Filter by team" -msgstr "" - msgid "Every day (at 4:00am)" msgstr "" @@ -513,9 +489,6 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" -msgid "Issue events" -msgstr "" - msgid "Jobs for last month" msgstr "" @@ -545,12 +518,6 @@ msgstr "" msgid "Last commit" msgstr "" -msgid "LastPushEvent|You pushed to" -msgstr "" - -msgid "LastPushEvent|at" -msgstr "" - msgid "Learn more in the" msgstr "" @@ -571,9 +538,6 @@ msgstr[1] "" msgid "Median" msgstr "" -msgid "Merge events" -msgstr "" - msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" @@ -777,9 +741,6 @@ msgstr "" msgid "Pipeline|with stages" msgstr "" -msgid "Project" -msgstr "" - msgid "Project '%{project_name}' queued for deletion." msgstr "" @@ -813,9 +774,6 @@ msgstr "" msgid "Project home" msgstr "" -msgid "ProjectActivityRSS|Subscribe" -msgstr "" - msgid "ProjectFeature|Disabled" msgstr "" @@ -837,9 +795,6 @@ msgstr "" msgid "ProjectNetworkGraph|Graph" msgstr "" -msgid "Push events" -msgstr "" - msgid "Read more" msgstr "" @@ -970,9 +925,6 @@ msgstr "" msgid "Target Branch" msgstr "" -msgid "Team" -msgstr "" - msgid "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." msgstr "" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po index 4617de25a7c..8f25c893ecd 100644 --- a/locale/eo/gitlab.po +++ b/locale/eo/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:53-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:21-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Esperanto\n" "Language: eo_UY\n" @@ -57,9 +57,18 @@ msgstr "Aro da diagramoj pri la seninterrompa integrado" msgid "About auto deploy" msgstr "Pri la aÅtomata disponigado" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Aktiva" @@ -84,6 +93,12 @@ msgstr "Aldoni novan dosierujon" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Arkivita projekto! La deponejo permesas nur legadon" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Alkroĉu dosieron per Åovmetado aÅ %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "Branĉo" @@ -137,6 +209,9 @@ msgstr "Elekti dosierojn" msgid "ByAuthor|by" msgstr "de" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "Agordoj de seninterrompa integrado" @@ -164,6 +239,9 @@ msgstr "Listo de ÅanÄoj" msgid "Charts" msgstr "Diagramoj" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Precize elekti ĉi tiun kunmetadon" @@ -259,12 +337,18 @@ msgstr "Enmetita de" msgid "Compare" msgstr "Kompari" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "Gvidlinioj por kontribuado" msgid "Contributors" msgstr "Kontribuantoj" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Kopii la adreson en la kopibufron" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "Disponigado" msgstr[1] "Disponigadoj" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "Priskribo" @@ -399,6 +486,9 @@ msgstr "Redakti" msgid "Edit Pipeline Schedule %{id}" msgstr "Redakti ĉenstablan planon %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "De la kreado de la problemo Äis la disponigado en la publika versio" msgid "From merge request merge until deploy to production" msgstr "De la kunfandado de la peto pri kunfando Äis la disponigado en la publika versio" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Al via disbranĉigo" msgid "GoToYourFork|Fork" msgstr "Disbranĉigo" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "Hejmo" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "La refreÅigo komenciÄis sukcese" @@ -515,14 +617,8 @@ msgstr "Ni prezentas al vi la ciklan analizon" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Taskoj po la lasta monato" - -msgid "Jobs for last week" -msgstr "Taskoj po la lasta semajno" - -msgid "Jobs for last year" -msgstr "Taskoj po la lasta jaro" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "MalÅaltita" @@ -530,6 +626,9 @@ msgstr "MalÅaltita" msgid "LFSStatus|Enabled" msgstr "Åœaltita" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "La lasta %d tago" @@ -562,20 +661,38 @@ msgstr "Forlasi la grupon" msgid "Leave project" msgstr "Forlasi la projekton" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limigita al montrado de ne pli ol %d evento" msgstr[1] "Limigita al montrado de ne pli ol %d eventoj" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Mediano" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "aldonos SSH-Ålosilon" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "Partoprenado" msgid "NotificationLevel|Watch" msgstr "Rigardado" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Filtrilo" @@ -686,9 +806,15 @@ msgstr "Malfermita" msgid "Options" msgstr "Opcioj" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "Posedanto" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "Ĉenstablo" @@ -701,6 +827,9 @@ msgstr "Ĉenstabla plano" msgid "Pipeline Schedules" msgstr "Ĉenstablaj planoj" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Malsukcesaj:" @@ -764,6 +893,15 @@ msgstr "Ĉenstabloj" msgid "Pipelines charts" msgstr "Ĉenstablaj diagramoj" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "ĉiuj" @@ -776,6 +914,12 @@ msgstr "kun etapo" msgid "Pipeline|with stages" msgstr "kun etapoj" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "La elporto de la projekto komenciÄis. Vi ricevos ligilon per retpoÅto msgid "Project home" msgstr "Hejmo de la projekto" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Etapo" msgid "ProjectNetworkGraph|Graph" msgstr "Grafeo" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "Malfari ĉi tiun enmetadon" msgid "Revert this merge request" msgstr "Malfari ĉi tiun peton pri kunfando" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Konservi ĉenstablan planon" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Elektu celan branĉon" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuÅi per %{protocol}." @@ -935,14 +1091,23 @@ msgstr "Agordi aÅtomatan disponigadon" msgid "SetPasswordToCloneLink|set a password" msgstr "kreos pasvorton" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Estas montrata %d evento" msgstr[1] "Estas montrataj %d eventoj" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Kodo" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto. msgid "We don't have enough data to show this stage." msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Nuligi la peton pri atingeblo" @@ -1284,4 +1452,5 @@ msgstr "sciigoj per retpoÅto" msgid "parent" msgid_plural "parents" msgstr[0] "patro" -msgstr[1] "patroj"
\ No newline at end of file +msgstr[1] "patroj" + diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 8158bd275bd..eee720d5ba2 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:37-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Spanish\n" "Language: es_ES\n" @@ -57,9 +57,18 @@ msgstr "Una colección de gráficos sobre Integración Continua" msgid "About auto deploy" msgstr "Acerca del auto despliegue" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Activo" @@ -84,6 +93,12 @@ msgstr "Agregar nuevo directorio" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "¡Proyecto archivado! El repositorio es de solo lectura" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Adjunte un archivo arrastrando & soltando o %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "Rama" @@ -137,6 +209,9 @@ msgstr "Examinar archivos" msgid "ByAuthor|by" msgstr "por" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "Configuración de CI" @@ -164,6 +239,9 @@ msgstr "" msgid "Charts" msgstr "Gráficos" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Escoger este cambio" @@ -259,12 +337,18 @@ msgstr "Enviado por" msgid "Compare" msgstr "Comparar" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "GuÃa de contribución" msgid "Contributors" msgstr "Contribuidores" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Copiar URL al portapapeles" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "Despliegue" msgstr[1] "Despliegues" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "Descripción" @@ -399,6 +486,9 @@ msgstr "Editar" msgid "Edit Pipeline Schedule %{id}" msgstr "Editar Programación del Pipeline %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "Desde la creación de la incidencia hasta el despliegue a producción" msgid "From merge request merge until deploy to production" msgstr "Desde la integración de la solicitud de fusión hasta el despliegue a producción" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Ir a tu bifurcación" msgid "GoToYourFork|Fork" msgstr "Bifurcación" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "Inicio" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "Servicio de limpieza iniciado con éxito" @@ -515,14 +617,8 @@ msgstr "Introducción a Cycle Analytics" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Trabajos del mes pasado" - -msgid "Jobs for last week" -msgstr "Trabajos de la semana pasada" - -msgid "Jobs for last year" -msgstr "Trabajos del año pasado" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Deshabilitado" @@ -530,6 +626,9 @@ msgstr "Deshabilitado" msgid "LFSStatus|Enabled" msgstr "Habilitado" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "Último %d dÃa" @@ -562,20 +661,38 @@ msgstr "Abandonar grupo" msgid "Leave project" msgstr "Abandonar proyecto" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limitado a mostrar máximo %d evento" msgstr[1] "Limitado a mostrar máximo %d eventos" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Mediana" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "agregar una clave SSH" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "Participación" msgid "NotificationLevel|Watch" msgstr "Vigilancia" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Filtrar" @@ -686,9 +806,15 @@ msgstr "Abierto" msgid "Options" msgstr "Opciones" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "Propietario" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "" @@ -701,6 +827,9 @@ msgstr "Programación del Pipeline" msgid "Pipeline Schedules" msgstr "Programaciones de los Pipelines" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Fallidos:" @@ -764,6 +893,15 @@ msgstr "" msgid "Pipelines charts" msgstr "Gráficos de los pipelines" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "todos" @@ -776,6 +914,12 @@ msgstr "con etapa" msgid "Pipeline|with stages" msgstr "con etapas" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descar msgid "Project home" msgstr "Inicio del proyecto" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Etapa" msgid "ProjectNetworkGraph|Graph" msgstr "Historial gráfico" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "Revertir este cambio" msgid "Revert this merge request" msgstr "Revertir esta solicitud de fusión" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Guardar programación del pipeline" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Selecciona una rama de destino" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}." @@ -935,14 +1091,23 @@ msgstr "Configurar auto despliegue" msgid "SetPasswordToCloneLink|set a password" msgstr "establecer una contraseña" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Mostrando %d evento" msgstr[1] "Mostrando %d eventos" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Código fuente" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador." msgid "We don't have enough data to show this stage." msgstr "No hay suficientes datos para mostrar en esta etapa." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Retirar Solicitud de Acceso" @@ -1284,4 +1452,5 @@ msgstr "correos electrónicos de notificación" msgid "parent" msgid_plural "parents" msgstr[0] "padre" -msgstr[1] "padres"
\ No newline at end of file +msgstr[1] "padres" + diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po index 3daff3f5c19..43e66d8dea4 100644 --- a/locale/fr/gitlab.po +++ b/locale/fr/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:53-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: French\n" "Language: fr_FR\n" @@ -57,9 +57,18 @@ msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)" msgid "About auto deploy" msgstr "A propos de l'auto-déploiement" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Actif" @@ -84,6 +93,12 @@ msgstr "Ajouter un nouveau dossier" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Projet archivé ! Le dépôt est en lecture seule" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Attachez un fichier par glisser & déposer ou %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "" @@ -137,6 +209,9 @@ msgstr "Parcourir les fichiers" msgid "ByAuthor|by" msgstr "par" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "Configuration de l'intégration continue (CI)" @@ -164,6 +239,9 @@ msgstr "Journal des modifications" msgid "Charts" msgstr "Graphiques" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Sélectionner cette validation" @@ -259,12 +337,18 @@ msgstr "Validé par" msgid "Compare" msgstr "Comparer" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "Guilde de contribution" msgid "Contributors" msgstr "Contributeurs" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Copier l'URL dans le presse-papier" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "Déploiement" msgstr[1] "Déploiements" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "" @@ -399,6 +486,9 @@ msgstr "Éditer" msgid "Edit Pipeline Schedule %{id}" msgstr "Éditer le pipeline programmé %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "Depuis la création de l'incident jusqu'au déploiement en production" msgid "From merge request merge until deploy to production" msgstr "Depuis la fusion de la demande de fusion jusqu'au déploiement en production" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Aller à votre fourche" msgid "GoToYourFork|Fork" msgstr "Fourche" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "Accueil" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "Maintenance démarrée avec succès" @@ -515,14 +617,8 @@ msgstr "Introduction à l'analyseur de cycle" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Tâches pour le mois dernier" - -msgid "Jobs for last week" -msgstr "Tâches pour la semaine dernière" - -msgid "Jobs for last year" -msgstr "Tâches pour l'année dernière" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Désactivé" @@ -530,6 +626,9 @@ msgstr "Désactivé" msgid "LFSStatus|Enabled" msgstr "Activé" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "Le dernier %d jour" @@ -562,20 +661,38 @@ msgstr "Quitter le groupe" msgid "Leave project" msgstr "Quitter le projet" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limiter l'affichage au plus à %d évènement" msgstr[1] "Limiter l'affichage au plus à %d évènements" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Médian" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "ajouter une clef SSH" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "Participation" msgid "NotificationLevel|Watch" msgstr "Surveillé" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Filtre" @@ -686,9 +806,15 @@ msgstr "Ouvert" msgid "Options" msgstr "" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "Propriétaire" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "" @@ -701,6 +827,9 @@ msgstr "Programmation de pipeline" msgid "Pipeline Schedules" msgstr "Programmations de pipeline" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Échecs : " @@ -764,6 +893,15 @@ msgstr "" msgid "Pipelines charts" msgstr "Graphique des pipelines" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "Tous" @@ -776,6 +914,12 @@ msgstr "avec l'étape" msgid "Pipeline|with stages" msgstr "avec les étapes" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "L'export du projet a débuté. Un lien de téléchargement sera envoyé msgid "Project home" msgstr "Accueil du projet" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Étape" msgid "ProjectNetworkGraph|Graph" msgstr "Graphique " +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "Annuler cette validation" msgid "Revert this merge request" msgstr "Annuler cette demande de fusion" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Sauvegarder le pipeline programmé" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Sélectionnez une branche cible" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par %{protocol}." @@ -935,14 +1091,23 @@ msgstr "Mettre en place l’auto-déploiement" msgid "SetPasswordToCloneLink|set a password" msgstr "définir un mot de passe" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Affichage de %d évènement" msgstr[1] "Affichage de %d évènements" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Code source" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pou msgid "We don't have enough data to show this stage." msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Retirer la demande d'accès" @@ -1284,4 +1452,5 @@ msgstr "courriels de notification" msgid "parent" msgid_plural "parents" msgstr[0] "" -msgstr[1] ""
\ No newline at end of file +msgstr[1] "" + diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2b7c6f7ad33..e5cf2aeb513 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-24 09:29+0200\n" -"PO-Revision-Date: 2017-08-24 09:29+0200\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 08:32+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -58,9 +58,18 @@ msgstr "" msgid "About auto deploy" msgstr "" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "" @@ -85,6 +94,12 @@ msgstr "" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "" @@ -106,6 +121,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "" @@ -138,6 +210,9 @@ msgstr "" msgid "ByAuthor|by" msgstr "" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "" @@ -165,6 +240,9 @@ msgstr "" msgid "Charts" msgstr "" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "" @@ -260,12 +338,18 @@ msgstr "" msgid "Compare" msgstr "" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "" msgid "Contributors" msgstr "" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "" @@ -352,6 +436,9 @@ msgid_plural "Deploys" msgstr[0] "" msgstr[1] "" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "" @@ -400,6 +487,9 @@ msgstr "" msgid "Edit Pipeline Schedule %{id}" msgstr "" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -465,6 +555,12 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -477,6 +573,9 @@ msgstr "" msgid "GoToYourFork|Fork" msgstr "" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -498,6 +597,9 @@ msgstr "" msgid "Home" msgstr "" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "" @@ -516,12 +618,18 @@ msgstr "" msgid "Issue events" msgstr "" +msgid "Issues" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" msgid "LFSStatus|Enabled" msgstr "" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "" @@ -554,20 +662,38 @@ msgstr "" msgid "Leave project" msgstr "" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" msgstr[1] "" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -669,6 +795,9 @@ msgstr "" msgid "NotificationLevel|Watch" msgstr "" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "" @@ -678,9 +807,15 @@ msgstr "" msgid "Options" msgstr "" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "" @@ -693,6 +828,9 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "" @@ -777,6 +915,12 @@ msgstr "" msgid "Pipeline|with stages" msgstr "" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -813,6 +957,9 @@ msgstr "" msgid "Project home" msgstr "" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -837,6 +984,9 @@ msgstr "" msgid "ProjectNetworkGraph|Graph" msgstr "" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -897,6 +1047,9 @@ msgstr "" msgid "Revert this merge request" msgstr "" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "" @@ -921,6 +1074,9 @@ msgstr "" msgid "Select target branch" msgstr "" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" @@ -936,14 +1092,23 @@ msgstr "" msgid "SetPasswordToCloneLink|set a password" msgstr "" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1220,6 +1385,9 @@ msgstr "" msgid "We don't have enough data to show this stage." msgstr "" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 7b8bea46e26..46b3e12f97c 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 10:25-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Italian\n" "Language: it_IT\n" @@ -57,9 +57,18 @@ msgstr "Un insieme di grafici riguardo la Continuous Integration" msgid "About auto deploy" msgstr "Riguardo il rilascio automatico" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Attivo" @@ -84,6 +93,12 @@ msgstr "Aggiungi una directory (cartella)" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Progetto archiviato! La Repository è sola-lettura" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Aggiungi un file tramite trascina & rilascia ( drag & drop) o %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "" @@ -137,6 +209,9 @@ msgstr "Guarda i files" msgid "ByAuthor|by" msgstr "per" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "Configurazione CI (Integrazione Continua)" @@ -164,6 +239,9 @@ msgstr "" msgid "Charts" msgstr "Grafici" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "" @@ -259,12 +337,18 @@ msgstr "Committato da " msgid "Compare" msgstr "Confronta" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "Guida per contribuire" msgid "Contributors" msgstr "Collaboratori" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Copia URL negli appunti" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "Rilascio" msgstr[1] "Rilasci" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "Descrizione" @@ -399,6 +486,9 @@ msgstr "Modifica" msgid "Edit Pipeline Schedule %{id}" msgstr "Cambia programmazione della pipeline %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "Dalla creazione di un issue fino al rilascio in produzione" msgid "From merge request merge until deploy to production" msgstr "Dalla richiesta di merge fino effettua il merge fino al rilascio in produzione" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Vai il tuo fork" msgid "GoToYourFork|Fork" msgstr "Fork" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "Housekeeping iniziato con successo" @@ -515,14 +617,8 @@ msgstr "Introduzione delle Analisi Cicliche" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Jobs dell'ultimo mese" - -msgid "Jobs for last week" -msgstr "Jobs dell'ultima settimana" - -msgid "Jobs for last year" -msgstr "Jobs dell'ultimo anno" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Disabilitato" @@ -530,6 +626,9 @@ msgstr "Disabilitato" msgid "LFSStatus|Enabled" msgstr "Abilitato" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "L'ultimo %d giorno" @@ -562,20 +661,38 @@ msgstr "Abbandona il gruppo" msgid "Leave project" msgstr "Abbandona il progetto" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limita visualizzazione %d d'evento" msgstr[1] "Limita visualizzazione %d di eventi" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Mediano" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "aggiungi una chiave SSH" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "Partecipa" msgid "NotificationLevel|Watch" msgstr "Osserva" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Filtra" @@ -686,9 +806,15 @@ msgstr "Aperto" msgid "Options" msgstr "Opzioni" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "" @@ -701,6 +827,9 @@ msgstr "Pianificazione Pipeline" msgid "Pipeline Schedules" msgstr "Pianificazione multipla Pipeline" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Fallita:" @@ -764,6 +893,15 @@ msgstr "Pipeline" msgid "Pipelines charts" msgstr "Grafici pipeline" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "tutto" @@ -776,6 +914,12 @@ msgstr "con stadio" msgid "Pipeline|with stages" msgstr "con più stadi" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "Esportazione del progetto iniziata. Un link di download sarà inviato vi msgid "Project home" msgstr "Home di progetto" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Stadio" msgid "ProjectNetworkGraph|Graph" msgstr "Grafico" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "Ripristina questo commit" msgid "Revert this merge request" msgstr "Ripristina questa richiesta di merge" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Salva pianificazione pipeline" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Seleziona una branch di destinazione" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}." @@ -935,14 +1091,23 @@ msgstr "Configura il rilascio automatico" msgid "SetPasswordToCloneLink|set a password" msgstr "imposta una password" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Visualizza %d evento" msgstr[1] "Visualizza %d eventi" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Codice Sorgente" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazi msgid "We don't have enough data to show this stage." msgstr "Non ci sono sufficienti dati da mostrare su questo stadio" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Ritira richiesta d'accesso" @@ -1284,4 +1452,5 @@ msgstr "Notifiche via email" msgid "parent" msgid_plural "parents" msgstr[0] "" -msgstr[1] ""
\ No newline at end of file +msgstr[1] "" + diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po index 670ac2d9684..bc25b69c80a 100644 --- a/locale/ja/gitlab.po +++ b/locale/ja/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 10:14-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Japanese\n" "Language: ja_JP\n" @@ -53,9 +53,18 @@ msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•" msgid "About auto deploy" msgstr "自動デプãƒã‚¤ã«ã¤ã„ã¦" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "有効" @@ -80,6 +89,12 @@ msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’è¿½åŠ " msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "アーカイブ済ã¿ãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆï¼ï¼ˆãƒ¬ãƒã‚¸ãƒˆãƒªãƒ¼ã¯èªã¿å–り専用ã§ã™ï¼‰" @@ -101,6 +116,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "ドラッグ&ドãƒãƒƒãƒ—ã¾ãŸã¯ %{upload_link} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "ブランãƒ" @@ -132,6 +204,9 @@ msgstr "ファイルを表示" msgid "ByAuthor|by" msgstr "作者" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "CI è¨å®š" @@ -159,6 +234,9 @@ msgstr "変更履æ´" msgid "Charts" msgstr "ãƒãƒ£ãƒ¼ãƒˆ" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック" @@ -253,12 +331,18 @@ msgstr "コミット担当者: " msgid "Compare" msgstr "比較" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "貢献者å‘ã‘ガイド" msgid "Contributors" msgstr "貢献者" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "クリップボードã«URLをコピー" @@ -344,6 +428,9 @@ msgid "Deploy" msgid_plural "Deploys" msgstr[0] "デプãƒã‚¤" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "説明" @@ -392,6 +479,9 @@ msgstr "編集" msgid "Edit Pipeline Schedule %{id}" msgstr "パイプラインスケジュール %{id} を編集" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -456,6 +546,12 @@ msgstr "課題ãŒç™»éŒ²ã•ã‚Œã¦ã‹ã‚‰ãƒ—ãƒãƒ€ã‚¯ã‚·ãƒ§ãƒ³ã«ãƒ‡ãƒ—ãƒã‚¤ã•ã‚Œ msgid "From merge request merge until deploy to production" msgstr "マージリクエストãŒãƒžãƒ¼ã‚¸ã•ã‚Œã¦ã‹ã‚‰ãƒ—ãƒãƒ€ã‚¯ã‚·ãƒ§ãƒ³ã«ãƒ‡ãƒ—ãƒã‚¤ã•ã‚Œã‚‹ã¾ã§" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -468,6 +564,9 @@ msgstr "自分ã®ãƒ•ã‚©ãƒ¼ã‚¯ã¸ç§»å‹•" msgid "GoToYourFork|Fork" msgstr "フォーク" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -489,6 +588,9 @@ msgstr "" msgid "Home" msgstr "ホーム" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "ãƒã‚¦ã‚¹ã‚ーピングã¯æ£å¸¸ã«èµ·å‹•ã—ã¾ã—ãŸã€‚" @@ -507,14 +609,8 @@ msgstr "サイクル分æžã®ã”紹介" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "先月ã®ã‚¸ãƒ§ãƒ–" - -msgid "Jobs for last week" -msgstr "先週ã®ã‚¸ãƒ§ãƒ–" - -msgid "Jobs for last year" -msgstr "昨年ã®ã‚¸ãƒ§ãƒ–" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "無効" @@ -522,6 +618,9 @@ msgstr "無効" msgid "LFSStatus|Enabled" msgstr "有効" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "éŽåŽ»%d日間" @@ -553,19 +652,37 @@ msgstr "グループを離脱" msgid "Leave project" msgstr "プãƒã‚¸ã‚§ã‚¯ãƒˆã‚’離脱" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "イベント表示数を最大 %d 個ã«åˆ¶é™" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "ä¸å¤®å€¤" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "SSH éµã‚’è¿½åŠ " +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -666,6 +783,9 @@ msgstr "å‚åŠ " msgid "NotificationLevel|Watch" msgstr "ã™ã¹ã¦é€šçŸ¥" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "フィルター" @@ -675,9 +795,15 @@ msgstr "オープンã•ã‚ŒãŸã®ã¯" msgid "Options" msgstr "オプション" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "オーナー" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "パイプライン" @@ -690,6 +816,9 @@ msgstr "パイプラインスケジュール" msgid "Pipeline Schedules" msgstr "パイプラインスケジュール" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "失敗:" @@ -753,6 +882,15 @@ msgstr "パイプライン" msgid "Pipelines charts" msgstr "パイプラインãƒãƒ£ãƒ¼ãƒˆ" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "全件" @@ -765,6 +903,12 @@ msgstr "ステージã‚ã‚Š" msgid "Pipeline|with stages" msgstr "ステージã‚ã‚Š" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -801,6 +945,9 @@ msgstr "プãƒã‚¸ã‚§ã‚¯ãƒˆã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’開始ã—ã¾ã—ãŸã€‚ダウン msgid "Project home" msgstr "プãƒã‚¸ã‚§ã‚¯ãƒˆãƒ›ãƒ¼ãƒ " +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -825,6 +972,9 @@ msgstr "ステージ" msgid "ProjectNetworkGraph|Graph" msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -885,6 +1035,9 @@ msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’リãƒãƒ¼ãƒˆ" msgid "Revert this merge request" msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’リãƒãƒ¼ãƒˆ" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "パイプラインスケジュールをä¿å˜" @@ -909,6 +1062,9 @@ msgstr "" msgid "Select target branch" msgstr "ターゲットブランãƒã‚’é¸æŠž" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "%{protocol} プãƒã‚³ãƒˆãƒ«çµŒç”±ã§ãƒ—ルã€ãƒ—ッシュã™ã‚‹ãŸã‚ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ‘スワードをè¨å®šã€‚" @@ -924,13 +1080,22 @@ msgstr "自動デプãƒã‚¤ã‚’è¨å®š" msgid "SetPasswordToCloneLink|set a password" msgstr "パスワードをè¨å®š" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示ä¸" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "ソースコード" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1204,6 +1369,9 @@ msgstr "ã“ã®ãƒ‡ãƒ¼ã‚¿ã‚’å‚ç…§ã—ãŸã„ã§ã™ã‹ï¼Ÿã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ç®¡ msgid "We don't have enough data to show this stage." msgstr "データä¸è¶³ã®ãŸã‚ã€ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã®è¡¨ç¤ºã¯ã§ãã¾ã›ã‚“。" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "アクセスリクエストをå–り消ã™" @@ -1267,4 +1435,5 @@ msgstr "メール通知" msgid "parent" msgid_plural "parents" -msgstr[0] "親"
\ No newline at end of file +msgstr[0] "親" + diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po index df850115222..4baefdb9a3e 100644 --- a/locale/ko/gitlab.po +++ b/locale/ko/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 10:05-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Korean\n" "Language: ko_KR\n" @@ -28,20 +28,20 @@ msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "%{commit_timeago} ì— %{commit_author_link} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. " msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" +msgstr "%{number_of_failures} / %{maximum_failures} 실패. GitLab ì€ ë‹¤ìŒ ì‹œë„ì—ì„œ 성공하면 ì ‘ê·¼ì„ í—ˆìš©í• ê²ƒìž…ë‹ˆë‹¤." msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." -msgstr "" +msgstr "%{number_of_failures} / %{maximum_failures} 실패. GitLab ì€ %{number_of_seconds} ì´ˆ ê°„ ì ‘ê·¼ì„ ì œí•œí•˜ê² ìŠµë‹ˆë‹¤." msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." -msgstr "" +msgstr "%{number_of_failures} / %{maximum_failures} 실패. GitLab ì€ ìžë™ìœ¼ë¡œ 다시 ì‹œë„하지 않습니다. ë¬¸ì œê°€ í•´ê²°ë˜ë©´ ì €ìž¥ 공간 ì •ë³´ë¥¼ 초기화 해주세요. " msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" msgstr[0] "" msgid "(checkout the %{link} for information on how to install it)." -msgstr "" +msgstr "설치 ë°©ë²•ì— ëŒ€í•œ ì •ë³´ë¥¼ 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요." msgid "1 pipeline" msgid_plural "%d pipelines" @@ -53,7 +53,16 @@ msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ" msgid "About auto deploy" msgstr "ìžë™ ë°°í¬ ì •ë³´" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "오ë™ìž‘ì¤‘ì¸ ì €ìž¥ê³µê°„ì— ëŒ€í•œ ì ‘ê·¼ì´ ë³µêµ¬ ìž‘ì—…ì„ ìœ„í•´ ë§ˆìš´íŠ¸í• ìˆ˜ 있ë„ë¡ ìž„ì‹œë¡œ 허용ë˜ì—ˆìŠµë‹ˆë‹¤. ë¬¸ì œê°€ í•´ê²°ëœ í›„ 다시 ì ‘ê·¼ì„ í—ˆìš©í• ìˆ˜ 있게 ì €ìž¥ê³µê°„ ì •ë³´ë¥¼ 리셋 해주세요." + +msgid "Account" msgstr "" msgid "Active" @@ -78,6 +87,12 @@ msgid "Add new directory" msgstr "새 ë””ë ‰í† ë¦¬ 추가" msgid "All" +msgstr "ì „ì²´" + +msgid "Appearances" +msgstr "" + +msgid "Applications" msgstr "" msgid "Archived project! Repository is read-only" @@ -87,20 +102,77 @@ msgid "Are you sure you want to delete this pipeline schedule?" msgstr "ì´ íŒŒì´í”„ë¼ì¸ ìŠ¤ì¼€ì¥´ì„ ì‚ì œ í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "변경 ë‚´ìš©ì„ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" msgid "Are you sure you want to reset registration token?" -msgstr "" +msgstr "ë“±ë¡ í† í°ì„ 초기화 í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" msgid "Are you sure you want to reset the health check token?" -msgstr "" +msgstr "헬스 ì²´í¬ í† í°ì„ 초기화 í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" msgid "Are you sure?" -msgstr "" +msgstr "확실합니까?" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "드래그 & ë“œë¡ ë˜ëŠ” %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "브랜치" @@ -132,6 +204,9 @@ msgstr "íŒŒì¼ ì°¾ì•„ë³´ê¸°" msgid "ByAuthor|by" msgstr "작성ìž" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "CI ì„¤ì •" @@ -159,6 +234,9 @@ msgstr "변경사í•" msgid "Charts" msgstr "차트" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "ì´ ì»¤ë°‹ì„ Cherry-pick" @@ -253,12 +331,18 @@ msgstr "커밋한 사용ìž" msgid "Compare" msgstr "비êµ" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내" msgid "Contributors" msgstr "기여해 ì£¼ì‹ ë¶„ë“¤" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "URLì„ í´ë¦½ë³´ë“œì— 복사" @@ -269,7 +353,7 @@ msgid "Create New Directory" msgstr "새 ë””ë ‰í† ë¦¬ 만들기" msgid "Create a new branch" -msgstr "" +msgstr "새 브랜치 ìƒì„±" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "%{protocol}ì„ (를) 통해 Pull 하거나 Push í• ê°œì¸ ì•¡ì„¸ìŠ¤ í† í°ì„ 만드ì‹ì‹œì˜¤." @@ -344,17 +428,20 @@ msgid "Deploy" msgid_plural "Deploys" msgstr[0] "ë°°í¬" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "설명" msgid "Details" -msgstr "" +msgstr "ìƒì„¸" msgid "Directory name" msgstr "ë””ë ‰í† ë¦¬ ì´ë¦„" msgid "Discard changes" -msgstr "" +msgstr "변경 ë‚´ìš© 취소" msgid "Don't show again" msgstr "다시 표시하지 ì•ŠìŒ" @@ -392,23 +479,26 @@ msgstr "편집" msgid "Edit Pipeline Schedule %{id}" msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}" -msgid "EventFilterBy|Filter by all" +msgid "Emails" msgstr "" +msgid "EventFilterBy|Filter by all" +msgstr "ëª¨ë“ ê°’ì„ ê¸°ì¤€ìœ¼ë¡œ í•„í„°" + msgid "EventFilterBy|Filter by comments" -msgstr "" +msgstr "댓글 기준으로 í•„í„°" msgid "EventFilterBy|Filter by issue events" -msgstr "" +msgstr "ì´ìŠˆ ì´ë²¤íŠ¸ 기준으로 í•„í„°" msgid "EventFilterBy|Filter by merge events" -msgstr "" +msgstr "머지 ì´ë²¤íŠ¸ 기준으로 í•„í„°" msgid "EventFilterBy|Filter by push events" -msgstr "" +msgstr "푸쉬 ì´ë²¤íŠ¸ 기준으로 í•„í„°" msgid "EventFilterBy|Filter by team" -msgstr "" +msgstr "팀 기준으로 í•„í„°" msgid "Every day (at 4:00am)" msgstr "ë§¤ì¼ (ì˜¤ì „ 4ì‹œì—)" @@ -456,39 +546,51 @@ msgstr "ì´ìŠˆ ìƒì„±ì—ì„œ 프로ë•ì…˜ ë°°í¬ê¹Œì§€" msgid "From merge request merge until deploy to production" msgstr "머지 리퀘스트 머지ì—ì„œ 프로ë•ì…˜ í™˜ê²½ì— ë°°í¬ê¹Œì§€" -msgid "Git storage health information has been reset" +msgid "GPG Keys" msgstr "" -msgid "GitLab Runner section" +msgid "Geo Nodes" msgstr "" +msgid "Git storage health information has been reset" +msgstr "git storage ìƒíƒœ ì •ë³´ê°€ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤." + +msgid "GitLab Runner section" +msgstr "GitLab Runner 섹션" + msgid "Go to your fork" msgstr "ë‹¹ì‹ ì˜ í¬í¬ë¡œ ì´ë™í•˜ì„¸ìš”" msgid "GoToYourFork|Fork" msgstr "í¬í¬" -msgid "Health Check" +msgid "Group overview" msgstr "" +msgid "Health Check" +msgstr "헬스 ì²´í¬" + msgid "Health information can be retrieved from the following endpoints. More information is available" -msgstr "" +msgstr "헬스 ì •ë³´ëŠ” 다ìŒì˜ 경로를 통해 ì¡°íšŒí• ìˆ˜ 있습니다. ë” ë§Žì€ ì •ë³´ë¥¼ ì´ìš©í• 수 있습니다." msgid "HealthCheck|Access token is" -msgstr "" +msgstr "엑세스 í† í°: " msgid "HealthCheck|Healthy" -msgstr "" +msgstr "ê±´ê°•ë„" msgid "HealthCheck|No Health Problems Detected" -msgstr "" +msgstr " 헬스 ë¬¸ì œê°€ 발견ë˜ì§€ 않았습니다." msgid "HealthCheck|Unhealthy" -msgstr "" +msgstr "ë¹„ì •ìƒ" msgid "Home" msgstr "홈" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "Housekeepingì´ ì„±ê³µì 으로 시작ë˜ì—ˆìŠµë‹ˆë‹¤" @@ -496,7 +598,7 @@ msgid "Import repository" msgstr "ì €ìž¥ì†Œ ê°€ì ¸ 오기" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "GitLab CI 와 호환ë˜ëŠ” Runner 설치" msgid "Interval Pattern" msgstr "주기 패턴" @@ -505,16 +607,10 @@ msgid "Introducing Cycle Analytics" msgstr "Cycle Analytics 소개" msgid "Issue events" -msgstr "" - -msgid "Jobs for last month" -msgstr "지난달 Jobs" +msgstr "ì´ìŠˆ ì´ë²¤íŠ¸" -msgid "Jobs for last week" -msgstr "지난주 Jobs" - -msgid "Jobs for last year" -msgstr "지난해 Jobs" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Disabled" @@ -522,6 +618,9 @@ msgstr "Disabled" msgid "LFSStatus|Enabled" msgstr "Enabled" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "최근 %d ì¼" @@ -530,16 +629,16 @@ msgid "Last Pipeline" msgstr "최근 파ì´í”„ë¼ì¸" msgid "Last Update" -msgstr "최근 ì—…ë°ì´íŠ¸:" +msgstr "최근 ì—…ë°ì´íŠ¸" msgid "Last commit" msgstr "최근 커밋" msgid "LastPushEvent|You pushed to" -msgstr "" +msgstr "푸쉬: " msgid "LastPushEvent|at" -msgstr "" +msgstr "at" msgid "Learn more in the" msgstr "ë” ìžì„¸ížˆ 알아보기" @@ -553,22 +652,40 @@ msgstr "그룹 ë– ë‚˜ê¸°" msgid "Leave project" msgstr "프로ì 트ì—ì„œ 나가기" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "최대 %d ì´ë²¤íŠ¸ 만 표시하는 것으로 ì œí•œë©ë‹ˆë‹¤." +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "중앙값" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" +msgstr "머지 ì´ë²¤íŠ¸" + +msgid "Messages" msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "SSH 키 추가" -msgid "More information is available|here" +msgid "Monitoring" msgstr "" +msgid "More information is available|here" +msgstr "여기" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "새 ì´ìŠˆ" @@ -649,7 +766,7 @@ msgid "NotificationEvent|Successful pipeline" msgstr "성공ì ì¸ íŒŒì´í”„ë¼ì¸" msgid "NotificationLevel|Custom" -msgstr "커스텀" +msgstr "ì‚¬ìš©ìž ì •ì˜" msgid "NotificationLevel|Disabled" msgstr "사용 안 함" @@ -666,6 +783,9 @@ msgstr "참여" msgid "NotificationLevel|Watch" msgstr "Watch" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "í•„í„°" @@ -675,9 +795,15 @@ msgstr "열린" msgid "Options" msgstr "옵션 " +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "ì†Œìœ ìž" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "파ì´í”„ë¼ì¸" @@ -690,6 +816,9 @@ msgstr "파ì´í”„ë¼ì¸ 스케쥴" msgid "Pipeline Schedules" msgstr "파ì´í”„ë¼ì¸ 스케쥴" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "실패 :" @@ -753,6 +882,15 @@ msgstr "파ì´í”„ë¼ì¸" msgid "Pipelines charts" msgstr "파ì´í”„ë¼ì¸ 차트" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "모ë‘" @@ -765,6 +903,12 @@ msgstr "스테ì´ì§•" msgid "Pipeline|with stages" msgstr "스테ì´ì§•" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -784,7 +928,7 @@ msgid "Project access must be granted explicitly to each user." msgstr "프로ì 트 액세스는 ê° ì‚¬ìš©ìžì—게 명시ì 으로 부여ë˜ì–´ì•¼í•©ë‹ˆë‹¤." msgid "Project details" -msgstr "" +msgstr "프로ì 트 ìƒì„¸" msgid "Project export could not be deleted." msgstr "프로ì 트 내보내기를 ì‚ì œí• ìˆ˜ 없습니다." @@ -801,9 +945,12 @@ msgstr "프로ì 트 내보내기가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. 다운로드 ë§í¬ë msgid "Project home" msgstr "프로ì 트 홈" -msgid "ProjectActivityRSS|Subscribe" +msgid "Project overview" msgstr "" +msgid "ProjectActivityRSS|Subscribe" +msgstr "구ë…" + msgid "ProjectFeature|Disabled" msgstr "사용 안 함" @@ -825,9 +972,12 @@ msgstr "스테ì´ì§•" msgid "ProjectNetworkGraph|Graph" msgstr "그래프" -msgid "Push events" +msgid "Push Rules" msgstr "" +msgid "Push events" +msgstr "푸쉬 ì´ë²¤íŠ¸" + msgid "Read more" msgstr "ë” ì½ê¸°" @@ -871,13 +1021,13 @@ msgid "Request Access" msgstr "액세스 ìš”ì²" msgid "Reset git storage health information" -msgstr "" +msgstr "git storage 헬스 ì •ë³´ 초기화" msgid "Reset health check access token" -msgstr "" +msgstr "헬스 ì²´í¬ ì ‘ê·¼ í† í° ì´ˆê¸°í™”" msgid "Reset runners registration token" -msgstr "" +msgstr "runner ë“±ë¡ í† í° ì´ˆê¸°í™”" msgid "Revert this commit" msgstr "ì´ ì»¤ë°‹ ë˜ëŒë¦¬ê¸°" @@ -885,6 +1035,9 @@ msgstr "ì´ ì»¤ë°‹ ë˜ëŒë¦¬ê¸°" msgid "Revert this merge request" msgstr "ì´ ë¨¸ì§€ 리퀘스트 ë˜ëŒë¦¬ê¸°" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "파ì´í”„ë¼ì¸ 스케줄 ì €ìž¥" @@ -909,6 +1062,9 @@ msgstr "" msgid "Select target branch" msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "%{protocol} í”„ë¡œí† ì½œì„ í†µí•´ Pull 하거나 Pushí•˜ë ¤ë©´ ê³„ì •ì— íŒ¨ìŠ¤ì›Œë“œë¥¼ ì„¤ì •í•˜ì‹ì‹œì˜¤." @@ -924,16 +1080,25 @@ msgstr "ìžë™ ë°°í¬ ì„¤ì •" msgid "SetPasswordToCloneLink|set a password" msgstr "패스워드 ì„¤ì •" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "소스 코드" -msgid "Specify the following URL during the Runner setup:" +msgid "Spam Logs" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "Runner ì„¤ì • 중 ë‹¤ìŒ URLì„ ì§€ì •í•˜ì„¸ìš”." + msgid "StarProject|Star" msgstr "별표" @@ -941,7 +1106,7 @@ msgid "Start a %{new_merge_request} with these changes" msgstr "ì´ ë³€ê²½ 사í•ìœ¼ë¡œ %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹ì‹œì˜¤." msgid "Start the Runner!" -msgstr "" +msgstr "Runner 시작!" msgid "Switch branch/tag" msgstr "스위치 브랜치/태그" @@ -1008,7 +1173,7 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet msgstr "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— ìžˆìŠµë‹ˆë‹¤. 예를 들어, 3, 5, 9 사ì´ì˜ 중간 ê°’ì€ 5입니다. 3, 5, 7, 8 사ì´ì˜ 중간 ê°’ì€ (5 + 7) / 2 = 6입니다." msgid "There are problems accessing Git storage: " -msgstr "" +msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. " msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "즉, 빈 ì €ìž¥ì†Œë¥¼ 만들거나 기존 ì €ìž¥ì†Œë¥¼ ê°€ì ¸ì˜¬ 때까지 코드를 Push í• ìˆ˜ 없습니다." @@ -1178,7 +1343,7 @@ msgid "UploadLink|click to upload" msgstr "ì—…ë¡œë“œí•˜ë ¤ë©´ í´ë¦í•˜ì‹ì‹œì˜¤." msgid "Use the following registration token during setup:" -msgstr "" +msgstr "ì„¤ì • ì¤‘ì— ë‹¤ìŒ ë“±ë¡ í† í° ì´ìš© : " msgid "Use your global notification setting" msgstr "ì „ì²´ 알림 ì„¤ì • 사용" @@ -1204,14 +1369,17 @@ msgstr "ì´ ë°ì´í„°ë¥¼ ë³´ê³ ì‹¶ì€ê°€ìš”? 관리ìžì—게 액세스 권한ì msgid "We don't have enough data to show this stage." msgstr "ì´ ë‹¨ê³„ë¥¼ ë³´ì—¬ì£¼ê¸°ì— ì¶©ë¶„í•œ ë°ì´í„°ê°€ 없습니다." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "액세스 ìš”ì² ì² íšŒ" msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "%{group_name} ê·¸ë£¹ì„ ì œê±°í•˜ë ¤ê³ í•©ë‹ˆë‹¤. \"ì •ë§ë¡œ\" 확실합니까?" +msgstr "%{group_name} ê·¸ë£¹ì„ ì œê±°í•˜ë ¤ê³ í•©ë‹ˆë‹¤. \\\"ì •ë§ë¡œ\\\" 확실합니까?" msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "%{project_name_with_namespace} 프로ì 트를 ì‚ì œí•˜ë ¤ê³ í•©ë‹ˆë‹¤. ì‚ì œëœ í”„ë¡œì 트를 ë³µì› í• ìˆ˜ 없습니다! \"ì •ë§ë¡œ\" 확실합니까?" +msgstr "%{project_name_with_namespace} 프로ì 트를 ì‚ì œí•˜ë ¤ê³ í•©ë‹ˆë‹¤. \"ì‚ì œëœ í”„ë¡œì 트를 ë³µì› í• ìˆ˜ 없습니다! \\\"ì •ë§ë¡œ\\\" 확실합니까?" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgstr "í¬í¬ 관계를 소스 프로ì 트 %{forked_from_project}ì— ëŒ€í•´ ì œê±°í•˜ë ¤ê³ í•©ë‹ˆë‹¤. \"ì •ë§ë¡œ\" 확실합니까?" @@ -1267,4 +1435,5 @@ msgstr "알림 ì´ë©”ì¼" msgid "parent" msgid_plural "parents" -msgstr[0] "부모"
\ No newline at end of file +msgstr[0] "부모" + diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po index d8887110867..88ca25dbb3b 100644 --- a/locale/pt_BR/gitlab.po +++ b/locale/pt_BR/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 10:14-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Portuguese, Brazilian\n" "Language: pt_BR\n" @@ -57,9 +57,18 @@ msgstr "Uma coleção de gráficos sobre Integração ContÃnua" msgid "About auto deploy" msgstr "Sobre o deploy automático" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Ativo" @@ -84,6 +93,12 @@ msgstr "Adicionar novo diretório" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Projeto arquivado! O repositório é somente leitura" @@ -105,6 +120,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "" @@ -137,6 +209,9 @@ msgstr "Navegar pelos arquivos" msgid "ByAuthor|by" msgstr "por" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "Configuração da IC" @@ -164,6 +239,9 @@ msgstr "Registro de mudanças" msgid "Charts" msgstr "Gráficos" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Cherry-pick esse commit" @@ -259,12 +337,18 @@ msgstr "Commit feito por" msgid "Compare" msgstr "Comparar" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "Guia de contribuição" msgid "Contributors" msgstr "Contribuidores" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Copiar URL para área de transferência" @@ -351,6 +435,9 @@ msgid_plural "Deploys" msgstr[0] "Implantação" msgstr[1] "Implantações" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "Descrição" @@ -399,6 +486,9 @@ msgstr "Alterar" msgid "Edit Pipeline Schedule %{id}" msgstr "Alterar Agendamento do Pipeline %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -464,6 +554,12 @@ msgstr "Da abertura de tarefas até a implantação para a produção" msgid "From merge request merge until deploy to production" msgstr "Do merge request até a implantação em produção" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -476,6 +572,9 @@ msgstr "Ir para seu fork" msgid "GoToYourFork|Fork" msgstr "Fork" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -497,6 +596,9 @@ msgstr "" msgid "Home" msgstr "InÃcio" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "Manutenção iniciada com sucesso" @@ -515,14 +617,8 @@ msgstr "Apresentando a Análise de Ciclo" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Jobs no último mês" - -msgid "Jobs for last week" -msgstr "Jobs na última semana" - -msgid "Jobs for last year" -msgstr "Jobs no último ano" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Desabilitado" @@ -530,6 +626,9 @@ msgstr "Desabilitado" msgid "LFSStatus|Enabled" msgstr "Habilitado" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "Último %d dia" @@ -562,20 +661,38 @@ msgstr "Sair do grupo" msgid "Leave project" msgstr "Sair do projeto" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limitado a mostrar %d evento, no máximo" msgstr[1] "Limitado a mostrar %d eventos, no máximo" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Mediana" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "adicione uma chave SSH" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -677,6 +794,9 @@ msgstr "Participar" msgid "NotificationLevel|Watch" msgstr "Observar" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Filtrar" @@ -686,9 +806,15 @@ msgstr "Aberto" msgid "Options" msgstr "Opções" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "Proprietário" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "" @@ -701,6 +827,9 @@ msgstr "Agendamento da Pipeline" msgid "Pipeline Schedules" msgstr "Agendamentos da Pipeline" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Falhou:" @@ -764,6 +893,15 @@ msgstr "" msgid "Pipelines charts" msgstr "Gráficos de pipelines" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "todos" @@ -776,6 +914,12 @@ msgstr "com etapa" msgid "Pipeline|with stages" msgstr "com etapas" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -812,6 +956,9 @@ msgstr "Exportação do projeto iniciada. Um link para baixá-la será enviado p msgid "Project home" msgstr "Página inicial do projeto" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -836,6 +983,9 @@ msgstr "Etapa" msgid "ProjectNetworkGraph|Graph" msgstr "Ãrvore" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -896,6 +1046,9 @@ msgstr "Reverter este commit" msgid "Revert this merge request" msgstr "Reverter esse merge request" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Salvar agendamento da pipeline" @@ -920,6 +1073,9 @@ msgstr "" msgid "Select target branch" msgstr "Selecionar branch de destino" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Defina uma senha para sua conta para aceitar ou entregar código via %{protocol}." @@ -935,14 +1091,23 @@ msgstr "Configurar implantação automática" msgid "SetPasswordToCloneLink|set a password" msgstr "defina uma senha" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Mostrando %d evento" msgstr[1] "Mostrando %d eventos" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Código-fonte" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1219,6 +1384,9 @@ msgstr "Precisa visualizar os dados? Solicite acesso ao administrador." msgid "We don't have enough data to show this stage." msgstr "Esta etapa não possui dados suficientes para exibição." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Remover Requisição de Acesso" @@ -1284,4 +1452,5 @@ msgstr "emails de notificação" msgid "parent" msgid_plural "parents" msgstr[0] "pai" -msgstr[1] "pais"
\ No newline at end of file +msgstr[1] "pais" + diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po index 926995d1f91..96e6c8a8d3f 100644 --- a/locale/ru/gitlab.po +++ b/locale/ru/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:41-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Russian\n" "Language: ru_RU\n" @@ -61,9 +61,18 @@ msgstr "Графики отноÑительно непрерывной интеРmsgid "About auto deploy" msgstr "ÐвтоматичеÑкое развертывание" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Ðктивный" @@ -88,6 +97,12 @@ msgstr "Добавить каталог" msgid "All" msgstr "" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Ðрхивный проект! Репозиторий доÑтупен только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ" @@ -95,7 +110,7 @@ msgid "Are you sure you want to delete this pipeline schedule?" msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñто раÑпиÑание конвейера?" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "Ð’Ñ‹ уверены, что Ð’Ñ‹ хотите отменить Ваши изменениÑ?" msgid "Are you sure you want to reset registration token?" msgstr "" @@ -104,11 +119,68 @@ msgid "Are you sure you want to reset the health check token?" msgstr "" msgid "Are you sure?" -msgstr "" +msgstr "Ð’Ñ‹ уверены?" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Приложить файл через drag & drop или %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "Ветка" @@ -142,6 +214,9 @@ msgstr "ПроÑмотр файлов" msgid "ByAuthor|by" msgstr "по автору" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "ÐаÑтройка CI" @@ -149,7 +224,7 @@ msgid "Cancel" msgstr "Отмена" msgid "Cancel edit" -msgstr "" +msgstr "Отменить редактирование" msgid "ChangeTypeActionLabel|Pick into branch" msgstr "Выбрать в ветке" @@ -169,6 +244,9 @@ msgstr "Журнал изменений" msgid "Charts" msgstr "Диаграммы" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Подобрать в Ñтом коммите" @@ -230,7 +308,7 @@ msgid "CiStatus|running" msgstr "выполнÑетÑÑ" msgid "Comments" -msgstr "" +msgstr "Комментарии" msgid "Commit" msgid_plural "Commits" @@ -265,12 +343,18 @@ msgstr "ФикÑировано" msgid "Compare" msgstr "Сравнить" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "РуководÑтво учаÑтника" msgid "Contributors" msgstr "УчаÑтники" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Копировать URL в буфер обмена" @@ -281,7 +365,7 @@ msgid "Create New Directory" msgstr "Создать директорию" msgid "Create a new branch" -msgstr "" +msgstr "Создать новую ветку" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через %{protocol}." @@ -358,6 +442,9 @@ msgstr[0] "РазмеÑтить" msgstr[1] "Размещение" msgstr[2] "Размещение" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "ОпиÑание" @@ -368,7 +455,7 @@ msgid "Directory name" msgstr "Каталог" msgid "Discard changes" -msgstr "" +msgstr "Отменить изменениÑ" msgid "Don't show again" msgstr "Ðе показывать Ñнова" @@ -406,6 +493,9 @@ msgstr "Редактировать" msgid "Edit Pipeline Schedule %{id}" msgstr "Изменить раÑпиÑание конвейера %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -472,11 +562,17 @@ msgstr "От ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹ до Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð msgid "From merge request merge until deploy to production" msgstr "От запроÑа на ÑлиÑние до Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² рабочей Ñреде" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" msgid "GitLab Runner section" -msgstr "" +msgstr "Ð¡ÐµÐºÑ†Ð¸Ñ Gitlab Runner" msgid "Go to your fork" msgstr "Перейти к вашему форку" @@ -484,6 +580,9 @@ msgstr "Перейти к вашему форку" msgid "GoToYourFork|Fork" msgstr "Форк" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -505,6 +604,9 @@ msgstr "" msgid "Home" msgstr "ГлавнаÑ" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "ОчиÑтка уÑпешно запущена" @@ -512,7 +614,7 @@ msgid "Import repository" msgstr "Импорт репозиториÑ" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "УÑтановите Gitlab Runner ÑовмеÑтимый Ñ Gitlab CI" msgid "Interval Pattern" msgstr "Шаблон интервала" @@ -523,14 +625,8 @@ msgstr "Внедрение Цикла Ðналитик" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "Работы за прошлый меÑÑц" - -msgid "Jobs for last week" -msgstr "Работы за прошлую неделю" - -msgid "Jobs for last year" -msgstr "Работы за прошлый год" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Отключено" @@ -538,6 +634,9 @@ msgstr "Отключено" msgid "LFSStatus|Enabled" msgstr "Включено" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "ПоÑледний %d день" @@ -571,21 +670,39 @@ msgstr "Покинуть группу" msgid "Leave project" msgstr "Покинуть проект" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Ограничение %d ÑобытиÑ" msgstr[1] "Ограничение %d Ñобытий" msgstr[2] "Ограничение %d Ñобытий" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Среднее" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "добавить ключ SSH" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -688,6 +805,9 @@ msgstr "УчаÑтие" msgid "NotificationLevel|Watch" msgstr "ОтÑлеживать" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Фильтр" @@ -697,9 +817,15 @@ msgstr "Открыто" msgid "Options" msgstr "ÐаÑтройки" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "Владелец" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "Конвейер" @@ -712,6 +838,9 @@ msgstr "РаÑпиÑание конвейера" msgid "Pipeline Schedules" msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð½Ð²ÐµÐ¹ÐµÑ€Ð¾Ð²" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Ðеудача:" @@ -775,6 +904,15 @@ msgstr "Конвейер" msgid "Pipelines charts" msgstr "Диаграмма конвейера" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "вÑе" @@ -787,6 +925,12 @@ msgstr "Ñо Ñтадией" msgid "Pipeline|with stages" msgstr "Ñо ÑтадиÑми" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -823,6 +967,9 @@ msgstr "Ðачат ÑкÑпорт проекта. СÑылка Ð´Ð»Ñ Ñкачи msgid "Project home" msgstr "ДомашнÑÑ Ñтраница" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -847,6 +994,9 @@ msgstr "Ðтап" msgid "ProjectNetworkGraph|Graph" msgstr "Граф" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -907,6 +1057,9 @@ msgstr "Отменить Ñто изменение" msgid "Revert this merge request" msgstr "Отменить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Сохранить раÑпиÑание конвейра" @@ -931,6 +1084,9 @@ msgstr "" msgid "Select target branch" msgstr "Выбор целевой ветки" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через %{protocol}." @@ -946,15 +1102,24 @@ msgstr "ÐаÑтройка автоматичеÑкого развертыван msgid "SetPasswordToCloneLink|set a password" msgstr "уÑтановить пароль" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Показано %d Ñобытие" msgstr[1] "Показано %d Ñобытий" msgstr[2] "Показано %d Ñобытий" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "ИÑходный код" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1234,6 +1399,9 @@ msgstr "Хотите увидеть данные? ОбратитеÑÑŒ к адм msgid "We don't have enough data to show this stage." msgstr "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Ñтапу отÑутÑтвует." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "Отменить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа" @@ -1301,4 +1469,5 @@ msgid "parent" msgid_plural "parents" msgstr[0] "иÑточник" msgstr[1] "иÑточники" -msgstr[2] "иÑточники"
\ No newline at end of file +msgstr[2] "иÑточники" + diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 5f9f087ff64..4d24140f3dc 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:49-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Ukrainian\n" "Language: uk_UA\n" @@ -61,9 +61,18 @@ msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ msgid "About auto deploy" msgstr "Про авто розгортаннÑ" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." msgstr "" +msgid "Account" +msgstr "" + msgid "Active" msgstr "Ðктивний" @@ -88,6 +97,12 @@ msgstr "Додати новий каталог" msgid "All" msgstr "Ð’ÑÑ–" +msgid "Appearances" +msgstr "" + +msgid "Applications" +msgstr "" + msgid "Archived project! Repository is read-only" msgstr "Заархівований проект! Репозиторій доÑтупний лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ" @@ -109,6 +124,63 @@ msgstr "" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "Гілка" @@ -142,6 +214,9 @@ msgstr "ПереглÑд файлів" msgid "ByAuthor|by" msgstr "від" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI" @@ -169,6 +244,9 @@ msgstr "СпиÑок змін (Changelog)" msgid "Charts" msgstr "Графіки" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "Cherry-pick в цьому комміті" @@ -265,12 +343,18 @@ msgstr "Комміт від" msgid "Compare" msgstr "ПорівнÑти" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "Керівництво контриб’юторів" msgid "Contributors" msgstr "Контриб’ютори" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "Скопіювати URL в буфер обміну" @@ -358,6 +442,9 @@ msgstr[0] "РозгортаннÑ" msgstr[1] "РозгортаннÑ" msgstr[2] "Розгортань" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "ОпиÑ" @@ -406,6 +493,9 @@ msgstr "Редагувати" msgid "Edit Pipeline Schedule %{id}" msgstr "Редагувати Розклад Конвеєра %{id}" +msgid "Emails" +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -472,6 +562,12 @@ msgstr "З моменту ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ до Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ msgid "From merge request merge until deploy to production" msgstr "З об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ð° ПРОД" +msgid "GPG Keys" +msgstr "" + +msgid "Geo Nodes" +msgstr "" + msgid "Git storage health information has been reset" msgstr "" @@ -484,6 +580,9 @@ msgstr "Перейти до вашого форку" msgid "GoToYourFork|Fork" msgstr "Форк" +msgid "Group overview" +msgstr "" + msgid "Health Check" msgstr "" @@ -505,6 +604,9 @@ msgstr "" msgid "Home" msgstr "Головна" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато" @@ -523,14 +625,8 @@ msgstr "ПредÑтавлÑємо аналітику циклу" msgid "Issue events" msgstr "" -msgid "Jobs for last month" -msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній міÑÑць" - -msgid "Jobs for last week" -msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній тиждень" - -msgid "Jobs for last year" -msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній рік" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "Вимкнено" @@ -538,6 +634,9 @@ msgstr "Вимкнено" msgid "LFSStatus|Enabled" msgstr "Увімкнено" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "ОÑтанній %d день" @@ -571,21 +670,39 @@ msgstr "Залишити групу" msgid "Leave project" msgstr "Залишити проект" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d події" msgstr[1] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій" msgstr[2] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "Медіана" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Messages" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "не додаÑте SSH ключ" +msgid "Monitoring" +msgstr "" + msgid "More information is available|here" msgstr "" @@ -688,6 +805,9 @@ msgstr "Берете учаÑÑ‚ÑŒ" msgid "NotificationLevel|Watch" msgstr "ВідÑтежувати" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "Фільтр" @@ -697,9 +817,15 @@ msgstr "Відкрито" msgid "Options" msgstr "Параметри" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "ВлаÑник" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "Конвеєр" @@ -712,6 +838,9 @@ msgstr "Розклад Конвеєра" msgid "Pipeline Schedules" msgstr "Розклади Конвеєрів" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "Ðе вдалоÑÑ:" @@ -775,6 +904,15 @@ msgstr "Конвеєри" msgid "Pipelines charts" msgstr "Чарти Конвеєрів" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "вÑÑ–" @@ -787,6 +925,12 @@ msgstr "зі Ñтадією" msgid "Pipeline|with stages" msgstr "зі ÑтадіÑми" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" +msgstr "" + msgid "Project" msgstr "" @@ -823,6 +967,9 @@ msgstr "Розпочато екÑпорт проекту. ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð msgid "Project home" msgstr "Ð”Ð¾Ð¼Ð°ÑˆÐ½Ñ Ñторінка проекту" +msgid "Project overview" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -847,6 +994,9 @@ msgstr "Етап" msgid "ProjectNetworkGraph|Graph" msgstr "ІÑторіÑ" +msgid "Push Rules" +msgstr "" + msgid "Push events" msgstr "" @@ -907,6 +1057,9 @@ msgstr "СкаÑувати цей комміт" msgid "Revert this merge request" msgstr "СкаÑувати цей запит на злиттÑ" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "Зберегти Розклад Конвеєра" @@ -931,6 +1084,9 @@ msgstr "" msgid "Select target branch" msgstr "Вибір цільової гілки" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати код через %{protocol}." @@ -946,15 +1102,24 @@ msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡Ð½Ðµ розгортаннÑ" msgid "SetPasswordToCloneLink|set a password" msgstr "вÑтановити пароль" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Показано %d подію" msgstr[1] "Показано %d події" msgstr[2] "Показано %d подій" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "Код" +msgid "Spam Logs" +msgstr "" + msgid "Specify the following URL during the Runner setup:" msgstr "" @@ -1234,6 +1399,9 @@ msgstr "Хочете побачити дані? Будь лаÑка, Ð¿Ð¾Ð¿Ñ€Ð¾Ñ msgid "We don't have enough data to show this stage." msgstr "Ми не маємо доÑтатньо даних Ð´Ð»Ñ Ð¿Ð¾ÐºÐ°Ð·Ñƒ цього етапу." +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "СкаÑувати запит доÑтупу" @@ -1301,4 +1469,5 @@ msgid "parent" msgid_plural "parents" msgstr[0] "джерело" msgstr[1] "джерела" -msgstr[2] "джерел"
\ No newline at end of file +msgstr[2] "джерел" + diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index eb607acf1f4..47de28209df 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:59-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Chinese Simplified\n" "Language: zh_CN\n" @@ -28,20 +28,20 @@ msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "ç”± %{commit_author_link} æ交于 %{commit_timeago}" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" +msgstr "已失败 %{number_of_failures} 次/最多å…许失败失败 %{maximum_failures} 次,GitLab 将继ç»é‡è¯•ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." -msgstr "" +msgstr "已失败 %{number_of_failures} 次/最多å…许失败 %{maximum_failures} 次,GitLab 将在 %{number_of_seconds} 秒åŽé‡è¯•ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." -msgstr "" +msgstr "已失败 %{number_of_failures} 次/最多å…许失败 %{maximum_failures} 次,GitLab ä¸ä¼šç»§ç»è‡ªåŠ¨é‡è¯•ã€‚请在问题解决åŽé‡ç½®å˜å‚¨å¥åº·ä¿¡æ¯ã€‚" msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" -msgstr[0] "" +msgstr[0] "%{storage_name}:已 %{failed_attempts} 次å°è¯•è®¿é—®å˜å‚¨å¤±è´¥ï¼š" msgid "(checkout the %{link} for information on how to install it)." -msgstr "" +msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})" msgid "1 pipeline" msgid_plural "%d pipelines" @@ -53,7 +53,16 @@ msgstr "æŒç»é›†æˆæ•°æ®å›¾" msgid "About auto deploy" msgstr "关于自动部署" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "为方便修å¤æŒ‚载问题,访问故障å˜å‚¨å·²è¢«æš‚æ—¶ç¦ç”¨ã€‚在问题解决åŽè¯·é‡ç½®å˜å‚¨å¥åº·ä¿¡æ¯ï¼Œä»¥å…许å†æ¬¡è®¿é—®ã€‚" + +msgid "Account" msgstr "" msgid "Active" @@ -78,6 +87,12 @@ msgid "Add new directory" msgstr "æ·»åŠ ç›®å½•" msgid "All" +msgstr "全部" + +msgid "Appearances" +msgstr "" + +msgid "Applications" msgstr "" msgid "Archived project! Repository is read-only" @@ -87,20 +102,77 @@ msgid "Are you sure you want to delete this pipeline schedule?" msgstr "确定è¦åˆ 除æ¤æµæ°´çº¿è®¡åˆ’å—?" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "确定è¦æ”¾å¼ƒä¿®æ”¹å—?" msgid "Are you sure you want to reset registration token?" -msgstr "" +msgstr "确定è¦é‡ç½®æ³¨å†Œä»¤ç‰Œå—?" msgid "Are you sure you want to reset the health check token?" -msgstr "" +msgstr "确定è¦é‡ç½®å¥åº·æ£€æŸ¥ä»¤ç‰Œå—?" msgid "Are you sure?" -msgstr "" +msgstr "确定å—?" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "拖放文件到æ¤å¤„或者 %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "分支" @@ -132,6 +204,9 @@ msgstr "æµè§ˆæ–‡ä»¶" msgid "ByAuthor|by" msgstr "作者:" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "CI é…ç½®" @@ -139,7 +214,7 @@ msgid "Cancel" msgstr "å–消" msgid "Cancel edit" -msgstr "" +msgstr "å–消编辑" msgid "ChangeTypeActionLabel|Pick into branch" msgstr "选择分支" @@ -159,6 +234,9 @@ msgstr "更新日志" msgid "Charts" msgstr "统计图" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "优选æ¤æ交" @@ -220,7 +298,7 @@ msgid "CiStatus|running" msgstr "è¿è¡Œä¸" msgid "Comments" -msgstr "" +msgstr "评论" msgid "Commit" msgid_plural "Commits" @@ -253,12 +331,18 @@ msgstr "æ交者:" msgid "Compare" msgstr "比较" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "贡献指å—" msgid "Contributors" msgstr "贡献者" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "å¤åˆ¶ URL 到剪贴æ¿" @@ -269,7 +353,7 @@ msgid "Create New Directory" msgstr "创建新目录" msgid "Create a new branch" -msgstr "" +msgstr "创建一个新分支" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "在å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ %{protocol} æ¥æ‹‰å–或推é€ã€‚" @@ -344,17 +428,20 @@ msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "æè¿°" msgid "Details" -msgstr "" +msgstr "详情" msgid "Directory name" msgstr "目录å称" msgid "Discard changes" -msgstr "" +msgstr "放弃更改" msgid "Don't show again" msgstr "ä¸å†æ˜¾ç¤º" @@ -392,23 +479,26 @@ msgstr "编辑" msgid "Edit Pipeline Schedule %{id}" msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’" -msgid "EventFilterBy|Filter by all" +msgid "Emails" msgstr "" +msgid "EventFilterBy|Filter by all" +msgstr "全部" + msgid "EventFilterBy|Filter by comments" -msgstr "" +msgstr "åªæ˜¾ç¤ºè¯„论事件" msgid "EventFilterBy|Filter by issue events" -msgstr "" +msgstr "åªæ˜¾ç¤ºè®®é¢˜äº‹ä»¶" msgid "EventFilterBy|Filter by merge events" -msgstr "" +msgstr "åªæ˜¾ç¤ºåˆå¹¶äº‹ä»¶" msgid "EventFilterBy|Filter by push events" -msgstr "" +msgstr "åªæ˜¾ç¤ºæŽ¨é€äº‹ä»¶" msgid "EventFilterBy|Filter by team" -msgstr "" +msgstr "åªæ˜¾ç¤ºå›¢é˜Ÿäº‹ä»¶" msgid "Every day (at 4:00am)" msgstr "æ¯æ—¥æ‰§è¡Œï¼ˆå‡Œæ™¨ 4 点)" @@ -456,39 +546,51 @@ msgstr "从创建议题到部署至生产环境" msgid "From merge request merge until deploy to production" msgstr "从åˆå¹¶è¯·æ±‚被åˆå¹¶åŽåˆ°éƒ¨ç½²è‡³ç”Ÿäº§çŽ¯å¢ƒ" -msgid "Git storage health information has been reset" +msgid "GPG Keys" msgstr "" -msgid "GitLab Runner section" +msgid "Geo Nodes" msgstr "" +msgid "Git storage health information has been reset" +msgstr "Git å˜å‚¨å¥åº·ä¿¡æ¯å·²é‡ç½®" + +msgid "GitLab Runner section" +msgstr "GitLab Runner" + msgid "Go to your fork" msgstr "跳转到派生项目" msgid "GoToYourFork|Fork" msgstr "跳转到派生项目" -msgid "Health Check" +msgid "Group overview" msgstr "" +msgid "Health Check" +msgstr "å¥åº·æ£€æŸ¥" + msgid "Health information can be retrieved from the following endpoints. More information is available" -msgstr "" +msgstr "å¥åº·ä¿¡æ¯å¯ä»¥ä»Žä»¥ä¸‹API路径获å–。如需了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹" msgid "HealthCheck|Access token is" -msgstr "" +msgstr "访问令牌是" msgid "HealthCheck|Healthy" -msgstr "" +msgstr "å¥åº·" msgid "HealthCheck|No Health Problems Detected" -msgstr "" +msgstr "没有检测到å¥åº·é—®é¢˜" msgid "HealthCheck|Unhealthy" -msgstr "" +msgstr "éžå¥åº·" msgid "Home" msgstr "首页" +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "已开始维护" @@ -496,7 +598,7 @@ msgid "Import repository" msgstr "导入å˜å‚¨åº“" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "安装一个与 GitLab CI 兼容的 Runner" msgid "Interval Pattern" msgstr "循环周期" @@ -505,16 +607,10 @@ msgid "Introducing Cycle Analytics" msgstr "周期分æžç®€ä»‹" msgid "Issue events" -msgstr "" - -msgid "Jobs for last month" -msgstr "上个月的作业" +msgstr "议题事件" -msgid "Jobs for last week" -msgstr "上个星期的作业" - -msgid "Jobs for last year" -msgstr "去年的作业" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "åœç”¨" @@ -522,6 +618,9 @@ msgstr "åœç”¨" msgid "LFSStatus|Enabled" msgstr "å¯ç”¨" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最近 %d 天" @@ -536,10 +635,10 @@ msgid "Last commit" msgstr "最åŽæ交" msgid "LastPushEvent|You pushed to" -msgstr "" +msgstr "您推é€äº†" msgid "LastPushEvent|at" -msgstr "" +msgstr "于" msgid "Learn more in the" msgstr "了解更多" @@ -553,22 +652,40 @@ msgstr "退出群组" msgid "Leave project" msgstr "退出项目" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "最多显示 %d 个事件" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "ä¸ä½æ•°" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" +msgstr "åˆå¹¶äº‹ä»¶" + +msgid "Messages" msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "新建 SSH 公钥" -msgid "More information is available|here" +msgid "Monitoring" msgstr "" +msgid "More information is available|here" +msgstr "帮助文档" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "新建议题" @@ -666,6 +783,9 @@ msgstr "å‚与" msgid "NotificationLevel|Watch" msgstr "关注" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "ç›é€‰" @@ -675,9 +795,15 @@ msgstr "开始于" msgid "Options" msgstr "æ“作" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "所有者" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "æµæ°´çº¿" @@ -690,6 +816,9 @@ msgstr "æµæ°´çº¿è®¡åˆ’" msgid "Pipeline Schedules" msgstr "æµæ°´çº¿è®¡åˆ’" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "失败:" @@ -753,6 +882,15 @@ msgstr "æµæ°´çº¿" msgid "Pipelines charts" msgstr "æµæ°´çº¿ç»Ÿè®¡å›¾" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "所有" @@ -765,9 +903,15 @@ msgstr "于阶段" msgid "Pipeline|with stages" msgstr "于阶段" -msgid "Project" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" msgstr "" +msgid "Project" +msgstr "项目" + msgid "Project '%{project_name}' queued for deletion." msgstr "项目 '%{project_name}' å·²è¿›å…¥åˆ é™¤é˜Ÿåˆ—ã€‚" @@ -784,7 +928,7 @@ msgid "Project access must be granted explicitly to each user." msgstr "项目访问æƒé™å¿…须明确授æƒç»™æ¯ä¸ªç”¨æˆ·ã€‚" msgid "Project details" -msgstr "" +msgstr "项目详情" msgid "Project export could not be deleted." msgstr "æ— æ³•åˆ é™¤é¡¹ç›®å¯¼å‡ºã€‚" @@ -801,9 +945,12 @@ msgstr "项目导出已开始。下载链接将通过电å邮件å‘é€ã€‚" msgid "Project home" msgstr "项目首页" -msgid "ProjectActivityRSS|Subscribe" +msgid "Project overview" msgstr "" +msgid "ProjectActivityRSS|Subscribe" +msgstr "订阅" + msgid "ProjectFeature|Disabled" msgstr "åœç”¨" @@ -825,9 +972,12 @@ msgstr "阶段" msgid "ProjectNetworkGraph|Graph" msgstr "分支图" -msgid "Push events" +msgid "Push Rules" msgstr "" +msgid "Push events" +msgstr "推é€äº‹ä»¶" + msgid "Read more" msgstr "了解更多" @@ -865,19 +1015,19 @@ msgid "Remove project" msgstr "åˆ é™¤é¡¹ç›®" msgid "Repository" -msgstr "" +msgstr "å˜å‚¨åº“" msgid "Request Access" msgstr "申请æƒé™" msgid "Reset git storage health information" -msgstr "" +msgstr "é‡ç½® Git å˜å‚¨çš„å¥åº·ä¿¡æ¯" msgid "Reset health check access token" -msgstr "" +msgstr "é‡ç½®å¥åº·æ£€æŸ¥è®¿é—®ä»¤ç‰Œ" msgid "Reset runners registration token" -msgstr "" +msgstr "é‡ç½® Runner 注册令牌" msgid "Revert this commit" msgstr "还原æ¤æ交" @@ -885,6 +1035,9 @@ msgstr "还原æ¤æ交" msgid "Revert this merge request" msgstr "还原æ¤åˆå¹¶è¯·æ±‚" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "ä¿å˜æµæ°´çº¿è®¡åˆ’" @@ -904,11 +1057,14 @@ msgid "Select a timezone" msgstr "选择时区" msgid "Select existing branch" -msgstr "" +msgstr "选择现有分支" msgid "Select target branch" msgstr "é€‰æ‹©ç›®æ ‡åˆ†æ”¯" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "为账å·åˆ›å»ºä¸€ä¸ªç”¨äºŽæŽ¨é€æˆ–拉å–çš„ %{protocol} 密ç 。" @@ -924,16 +1080,25 @@ msgstr "设置自动部署" msgid "SetPasswordToCloneLink|set a password" msgstr "设置密ç " +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "显示 %d 个事件" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "æºä»£ç " -msgid "Specify the following URL during the Runner setup:" +msgid "Spam Logs" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "在 Runner 设置时指定以下 URL:" + msgid "StarProject|Star" msgstr "æ˜Ÿæ ‡" @@ -941,7 +1106,7 @@ msgid "Start a %{new_merge_request} with these changes" msgstr "ç”±æ¤æ›´æ”¹ %{new_merge_request}" msgid "Start the Runner!" -msgstr "" +msgstr "å¯åŠ¨ Runner!" msgid "Switch branch/tag" msgstr "切æ¢åˆ†æ”¯/æ ‡ç¾" @@ -957,7 +1122,7 @@ msgid "Target Branch" msgstr "ç›®æ ‡åˆ†æ”¯" msgid "Team" -msgstr "" +msgstr "团队" msgid "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." msgstr "ç¼–ç 阶段概述了从第一次æ交到创建åˆå¹¶è¯·æ±‚的时间。创建第一个åˆå¹¶è¯·æ±‚åŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ 到æ¤å¤„。" @@ -1008,7 +1173,7 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet msgstr "ä¸ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—ä¸æœ€ä¸é—´çš„值。例如在 3ã€5ã€9 之间,ä¸ä½æ•°æ˜¯ 5。在 3ã€5ã€7ã€8 之间,ä¸ä½æ•°æ˜¯ (5 + 7)/ 2 = 6。" msgid "There are problems accessing Git storage: " -msgstr "" +msgstr "访问 Git å˜å‚¨æ—¶å‡ºçŽ°é—®é¢˜ï¼š" msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "在创建一个空的å˜å‚¨åº“或导入现有å˜å‚¨åº“之å‰ï¼Œå°†æ— 法推é€ä»£ç 。" @@ -1178,7 +1343,7 @@ msgid "UploadLink|click to upload" msgstr "ç‚¹å‡»ä¸Šä¼ " msgid "Use the following registration token during setup:" -msgstr "" +msgstr "在安装过程ä¸ä½¿ç”¨ä»¥ä¸‹æ³¨å†Œä»¤ç‰Œï¼š" msgid "Use your global notification setting" msgstr "使用全局通知设置" @@ -1204,6 +1369,9 @@ msgstr "æƒé™ä¸è¶³ã€‚如需查看相关数æ®ï¼Œè¯·å‘管ç†å‘˜ç”³è¯·æƒé™ã€‚ msgid "We don't have enough data to show this stage." msgstr "该阶段的数æ®ä¸è¶³ï¼Œæ— 法显示。" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "å–消æƒé™ç”³è¯·" @@ -1267,4 +1435,5 @@ msgstr "通知邮件" msgid "parent" msgid_plural "parents" -msgstr[0] "父级"
\ No newline at end of file +msgstr[0] "父级" + diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 74c7b464091..fee0d661c7a 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:59-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:21-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Chinese Traditional, Hong Kong\n" "Language: zh_HK\n" @@ -28,20 +28,20 @@ msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "ç”± %{commit_author_link} æ交於 %{commit_timeago}" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" +msgstr "已失敗 %{number_of_failures} 次,最大失敗 %{maximum_failures} 次,GitLab å°‡é‡è©¦ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." -msgstr "" +msgstr "已失敗 %{number_of_failures} 次,最大失敗 %{maximum_failures} 次,GitLab 將在 %{number_of_seconds} 秒後é‡è©¦ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." -msgstr "" +msgstr "已失敗 %{number_of_failures} 次,最大失敗 %{maximum_failures} 次,GitLabä¸æœƒé‡è©¦ã€‚當å•é¡Œè§£æ±ºæ™‚é‡ç½®å˜å„²ä¿¡æ¯ã€‚" msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" -msgstr[0] "" +msgstr[0] "%{storage_name}:已訪å•æ¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次" msgid "(checkout the %{link} for information on how to install it)." -msgstr "" +msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})" msgid "1 pipeline" msgid_plural "%d pipelines" @@ -53,7 +53,16 @@ msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ" msgid "About auto deploy" msgstr "關於自動部署" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "å› æ¢å¾©å®‰è£ï¼Œè¨ªå•æ•…éšœå˜å„²å·²è¢«æš«æ™‚ç¦ç”¨ã€‚在å•é¡Œè§£æ±ºå¾Œå°‡é‡ç½®å˜å„²ä¿¡æ¯ï¼Œä»¥ä¾¿å†æ¬¡è¨ªå•ã€‚" + +msgid "Account" msgstr "" msgid "Active" @@ -78,6 +87,12 @@ msgid "Add new directory" msgstr "æ·»åŠ æ–°ç›®éŒ„" msgid "All" +msgstr "全部" + +msgid "Appearances" +msgstr "" + +msgid "Applications" msgstr "" msgid "Archived project! Repository is read-only" @@ -87,20 +102,77 @@ msgid "Are you sure you want to delete this pipeline schedule?" msgstr "確定è¦åˆªé™¤æ¤æµæ°´ç·šè¨ˆåŠƒå—Žï¼Ÿ" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "確定è¦æ”¾æ£„修改嗎?" msgid "Are you sure you want to reset registration token?" -msgstr "" +msgstr "確定è¦é‡ç½®è¨»å†Šä»¤ç‰Œå—Žï¼Ÿ" msgid "Are you sure you want to reset the health check token?" -msgstr "" +msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥ä»¤ç‰Œå—Žï¼Ÿ" msgid "Are you sure?" -msgstr "" +msgstr "確定嗎?" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "拖放文件到æ¤è™•æˆ–者 %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "分支" @@ -132,6 +204,9 @@ msgstr "ç€è¦½æ–‡ä»¶" msgid "ByAuthor|by" msgstr "作者:" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "CI é…ç½®" @@ -139,7 +214,7 @@ msgid "Cancel" msgstr "å–消" msgid "Cancel edit" -msgstr "" +msgstr "å–消编辑" msgid "ChangeTypeActionLabel|Pick into branch" msgstr "挑é¸åˆ°åˆ†æ”¯" @@ -159,6 +234,9 @@ msgstr "更新日誌" msgid "Charts" msgstr "統計圖" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "優é¸æ¤æ交" @@ -220,7 +298,7 @@ msgid "CiStatus|running" msgstr "é‹è¡Œä¸" msgid "Comments" -msgstr "" +msgstr "è©•è«– (Comment)" msgid "Commit" msgid_plural "Commits" @@ -253,12 +331,18 @@ msgstr "æ交者:" msgid "Compare" msgstr "比較" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "è²¢ç»æŒ‡å—" msgid "Contributors" msgstr "è²¢ç»è€…" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "複製URL到剪貼æ¿" @@ -269,7 +353,7 @@ msgid "Create New Directory" msgstr "創建新目錄" msgid "Create a new branch" -msgstr "" +msgstr "創建壹個新分支 (branch)" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "在帳戶上創建個人訪å•ä»¤ç‰Œï¼Œä»¥é€šéŽ %{protocol} 來拉å–或推é€ã€‚" @@ -344,17 +428,20 @@ msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "æè¿°" msgid "Details" -msgstr "" +msgstr "詳情" msgid "Directory name" msgstr "目錄å稱" msgid "Discard changes" -msgstr "" +msgstr "放棄更改" msgid "Don't show again" msgstr "ä¸å†é¡¯ç¤º" @@ -392,23 +479,26 @@ msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ" -msgid "EventFilterBy|Filter by all" +msgid "Emails" msgstr "" +msgid "EventFilterBy|Filter by all" +msgstr "全部" + msgid "EventFilterBy|Filter by comments" -msgstr "" +msgstr "按評論 (comment) éŽæ¿¾" msgid "EventFilterBy|Filter by issue events" -msgstr "" +msgstr "按è°é¡Œäº‹ä»¶ (issue event) éŽæ¿¾" msgid "EventFilterBy|Filter by merge events" -msgstr "" +msgstr "按åˆä½µäº‹ä»¶ (merge event) éŽæ¿¾" msgid "EventFilterBy|Filter by push events" -msgstr "" +msgstr "按推é€äº‹ä»¶ (push event) éŽæ¿¾" msgid "EventFilterBy|Filter by team" -msgstr "" +msgstr "按團隊éŽæ¿¾" msgid "Every day (at 4:00am)" msgstr "æ¯æ—¥åŸ·è¡Œï¼ˆæ·©æ™¨ 4 點)" @@ -456,39 +546,51 @@ msgstr "從創建è°é¡Œåˆ°éƒ¨ç½²åˆ°ç”Ÿç”¢ç’°å¢ƒ" msgid "From merge request merge until deploy to production" msgstr "從åˆä½µè«‹æ±‚çš„åˆä½µåˆ°éƒ¨ç½²è‡³ç”Ÿç”¢ç’°å¢ƒ" -msgid "Git storage health information has been reset" +msgid "GPG Keys" msgstr "" -msgid "GitLab Runner section" +msgid "Geo Nodes" msgstr "" +msgid "Git storage health information has been reset" +msgstr "Git å˜å„²å¥åº·ä¿¡æ¯å·²é‡ç½®" + +msgid "GitLab Runner section" +msgstr "GitLab Runner 介紹" + msgid "Go to your fork" msgstr "è·³è½‰åˆ°æ´¾ç”Ÿé …ç›®" msgid "GoToYourFork|Fork" msgstr "è·³è½‰åˆ°æ´¾ç”Ÿé …ç›®" -msgid "Health Check" +msgid "Group overview" msgstr "" +msgid "Health Check" +msgstr "å¥åº·æª¢æŸ¥ (Health Check)" + msgid "Health information can be retrieved from the following endpoints. More information is available" -msgstr "" +msgstr "å¥åº·ä¿¡æ¯å¯ä»¥å¾žä»¥ä¸‹ç«¯é»žæª¢ç´¢ã€‚想了解更多信æ¯è«‹æŸ¥çœ‹" msgid "HealthCheck|Access token is" -msgstr "" +msgstr "訪å•ä»¤ç‰Œæ˜¯" msgid "HealthCheck|Healthy" -msgstr "" +msgstr "å¥åº·" msgid "HealthCheck|No Health Problems Detected" -msgstr "" +msgstr "沒有檢測到å¥åº·å•é¡Œ" msgid "HealthCheck|Unhealthy" -msgstr "" +msgstr "ä¸è‰¯" msgid "Home" msgstr "首é " +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "已開始ç¶è·" @@ -496,7 +598,7 @@ msgid "Import repository" msgstr "å°Žå…¥å˜å„²åº«" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "安è£å£¹å€‹èˆ‡ GitLab CI 兼容的 Runner" msgid "Interval Pattern" msgstr "循環週期" @@ -505,16 +607,10 @@ msgid "Introducing Cycle Analytics" msgstr "週期分æžç°¡ä»‹" msgid "Issue events" -msgstr "" - -msgid "Jobs for last month" -msgstr "上個月的作æ¥" +msgstr "è°é¡Œäº‹ä»¶ (issue event)" -msgid "Jobs for last week" -msgstr "上個星期的作æ¥" - -msgid "Jobs for last year" -msgstr "去年的作æ¥" +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "åœç”¨" @@ -522,6 +618,9 @@ msgstr "åœç”¨" msgid "LFSStatus|Enabled" msgstr "啟用" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最近 %d 天" @@ -536,10 +635,10 @@ msgid "Last commit" msgstr "最後æ交" msgid "LastPushEvent|You pushed to" -msgstr "" +msgstr "您推é€äº†" msgid "LastPushEvent|at" -msgstr "" +msgstr "在" msgid "Learn more in the" msgstr "了解更多" @@ -553,22 +652,40 @@ msgstr "退出群組" msgid "Leave project" msgstr "é€€å‡ºé …ç›®" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "最多顯示 %d 個事件" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "ä¸ä½æ•¸" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" +msgstr "åˆä½µäº‹ä»¶ (merge event)" + +msgid "Messages" msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "æ·»åŠ å£¹å€‹ SSH 公鑰" -msgid "More information is available|here" +msgid "Monitoring" msgstr "" +msgid "More information is available|here" +msgstr "幫助文檔" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "新建è°é¡Œ" @@ -666,6 +783,9 @@ msgstr "åƒèˆ‡" msgid "NotificationLevel|Watch" msgstr "關注" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "篩é¸" @@ -675,9 +795,15 @@ msgstr "開始於" msgid "Options" msgstr "æ“作" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "所有者" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "æµæ°´ç·š" @@ -690,6 +816,9 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ" msgid "Pipeline Schedules" msgstr "æµæ°´ç·šè¨ˆåŠƒ" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "失敗:" @@ -753,6 +882,15 @@ msgstr "æµæ°´ç·š" msgid "Pipelines charts" msgstr "æµæ°´ç·šåœ–表" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "所有" @@ -765,9 +903,15 @@ msgstr "於階段" msgid "Pipeline|with stages" msgstr "於階段" -msgid "Project" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" msgstr "" +msgid "Project" +msgstr "專案" + msgid "Project '%{project_name}' queued for deletion." msgstr "é …ç›® '%{project_name}' 已進入刪除隊列。" @@ -784,7 +928,7 @@ msgid "Project access must be granted explicitly to each user." msgstr "é …ç›®è¨ªå•æ¬Šé™å¿…é ˆæ˜Žç¢ºæŽˆæ¬Šçµ¦æ¯å€‹ç”¨æˆ¶ã€‚" msgid "Project details" -msgstr "" +msgstr "專案詳情" msgid "Project export could not be deleted." msgstr "ç„¡æ³•åˆªé™¤é …ç›®å°Žå‡ºã€‚" @@ -801,9 +945,12 @@ msgstr "é …ç›®å°Žå‡ºå·²é–‹å§‹ã€‚ä¸‹è¼‰éˆæŽ¥å°‡é€šéŽé›»å郵件發é€ã€‚" msgid "Project home" msgstr "é …ç›®é¦–é " -msgid "ProjectActivityRSS|Subscribe" +msgid "Project overview" msgstr "" +msgid "ProjectActivityRSS|Subscribe" +msgstr "訂閱" + msgid "ProjectFeature|Disabled" msgstr "åœç”¨" @@ -825,9 +972,12 @@ msgstr "階段" msgid "ProjectNetworkGraph|Graph" msgstr "分支圖" -msgid "Push events" +msgid "Push Rules" msgstr "" +msgid "Push events" +msgstr "推é€äº‹ä»¶ (push event) " + msgid "Read more" msgstr "了解更多" @@ -865,19 +1015,19 @@ msgid "Remove project" msgstr "åˆªé™¤é …ç›®" msgid "Repository" -msgstr "" +msgstr "å˜å„²åº«" msgid "Request Access" msgstr "申請權é™" msgid "Reset git storage health information" -msgstr "" +msgstr "é‡ç½® Git å˜å„²çš„å¥åº·ä¿¡æ¯" msgid "Reset health check access token" -msgstr "" +msgstr "é‡ç½®å¥åº·æª¢æŸ¥è¨ªå•ä»¤ç‰Œ" msgid "Reset runners registration token" -msgstr "" +msgstr "é‡ç½® Runner 註冊令牌" msgid "Revert this commit" msgstr "還原æ¤æ交" @@ -885,6 +1035,9 @@ msgstr "還原æ¤æ交" msgid "Revert this merge request" msgstr "還原æ¤åˆä½µè«‹æ±‚" +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "ä¿å˜æµæ°´ç·šè¨ˆåŠƒ" @@ -904,11 +1057,14 @@ msgid "Select a timezone" msgstr "é¸æ“‡æ™‚å€" msgid "Select existing branch" -msgstr "" +msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)" msgid "Select target branch" msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯" +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "ç‚ºè³¬è™Ÿæ·»åŠ å£¹å€‹ç”¨æ–¼æŽ¨é€æˆ–拉å–çš„ %{protocol} 密碼。" @@ -924,16 +1080,25 @@ msgstr "è¨ç½®è‡ªå‹•éƒ¨ç½²" msgid "SetPasswordToCloneLink|set a password" msgstr "è¨ç½®å¯†ç¢¼" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "æºä»£ç¢¼" -msgid "Specify the following URL during the Runner setup:" +msgid "Spam Logs" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "在 Runner è¨ç½®æ™‚指定以下 URL:" + msgid "StarProject|Star" msgstr "星標" @@ -941,7 +1106,7 @@ msgid "Start a %{new_merge_request} with these changes" msgstr "ç”±æ¤æ›´æ”¹ %{new_merge_request}" msgid "Start the Runner!" -msgstr "" +msgstr "é‹ä½œ Runner!" msgid "Switch branch/tag" msgstr "切æ›åˆ†æ”¯/標籤" @@ -957,7 +1122,7 @@ msgid "Target Branch" msgstr "目標分支" msgid "Team" -msgstr "" +msgstr "團隊" msgid "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." msgstr "編碼階段概述了從第壹次æ交到創建åˆä½µè«‹æ±‚的時間。創建第壹個åˆä½µè«‹æ±‚å¾Œï¼Œæ•¸æ“šå°‡è‡ªå‹•æ·»åŠ åˆ°æ¤è™•ã€‚" @@ -1008,7 +1173,7 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet msgstr "ä¸ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—ä¸æœ€ä¸é–“的值。例如在 3ã€5ã€9 之間,ä¸ä½æ•¸æ˜¯ 5。在 3ã€5ã€7ã€8 之間,ä¸ä½æ•¸æ˜¯ (5 + 7)/ 2 = 6。" msgid "There are problems accessing Git storage: " -msgstr "" +msgstr "è¨ªå• Git å˜å„²æ™‚出ç¾å•é¡Œï¼š" msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "在創建壹個空的å˜å„²åº«æˆ–å°Žå…¥ç¾æœ‰å˜å„²åº«ä¹‹å‰ï¼Œæ‚¨å°‡ç„¡æ³•æŽ¨é€ä»£ç¢¼ã€‚" @@ -1178,7 +1343,7 @@ msgid "UploadLink|click to upload" msgstr "點擊上傳" msgid "Use the following registration token during setup:" -msgstr "" +msgstr "在安è£éŽç¨‹ä¸ä½¿ç”¨ä»¥ä¸‹è¨»å†Šä»¤ç‰Œï¼š" msgid "Use your global notification setting" msgstr "使用全局通知è¨ç½®" @@ -1204,6 +1369,9 @@ msgstr "權é™ä¸è¶³ã€‚如需查看相關數據,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚ msgid "We don't have enough data to show this stage." msgstr "該階段的數據ä¸è¶³ï¼Œç„¡æ³•é¡¯ç¤ºã€‚" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "å–消權é™ç”³è¯·" @@ -1267,4 +1435,5 @@ msgstr "通知郵件" msgid "parent" msgid_plural "parents" -msgstr[0] "父級"
\ No newline at end of file +msgstr[0] "父級" + diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 1fc6b79187f..09c07a83d34 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab-ee\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-08-18 14:15+0530\n" -"PO-Revision-Date: 2017-08-23 09:59-0400\n" +"POT-Creation-Date: 2017-09-06 08:32+0200\n" +"PO-Revision-Date: 2017-09-06 06:20-0400\n" "Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n" "Language-Team: Chinese Traditional\n" "Language: zh_TW\n" @@ -28,20 +28,20 @@ msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "%{commit_author_link} 在 %{commit_timeago} é€äº¤" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." -msgstr "" +msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} æ¬¡å‰ GitLab 會é‡è©¦ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." -msgstr "" +msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} æ¬¡å‰ GitLab 會在 %{number_of_seconds} 秒後é‡è©¦ã€‚" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." msgstr "" msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" -msgstr[0] "" +msgstr[0] "%{storage_name}:已å˜å–æ¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次" msgid "(checkout the %{link} for information on how to install it)." -msgstr "" +msgstr "(如何安è£è«‹åƒé–± %{link})" msgid "1 pipeline" msgid_plural "%d pipelines" @@ -53,7 +53,16 @@ msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表" msgid "About auto deploy" msgstr "關於自動部署" +msgid "Abuse Reports" +msgstr "" + +msgid "Access Tokens" +msgstr "" + msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "已暫時åœç”¨å¤±æ•—çš„ Git 儲å˜ç©ºé–“。當儲å˜ç©ºé–“æ¢å¾©æ£å¸¸å¾Œï¼Œè«‹é‡ç½®å„²å˜ç©ºé–“å¥åº·æŒ‡æ•¸ã€‚" + +msgid "Account" msgstr "" msgid "Active" @@ -78,6 +87,12 @@ msgid "Add new directory" msgstr "新增目錄" msgid "All" +msgstr "全部" + +msgid "Appearances" +msgstr "" + +msgid "Applications" msgstr "" msgid "Archived project! Repository is read-only" @@ -87,20 +102,77 @@ msgid "Are you sure you want to delete this pipeline schedule?" msgstr "確定è¦åˆªé™¤æ¤æµæ°´ç·š (pipeline) 排程嗎?" msgid "Are you sure you want to discard your changes?" -msgstr "" +msgstr "確定è¦æ”¾æ£„修改嗎?" msgid "Are you sure you want to reset registration token?" -msgstr "" +msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘è‰ (registration token) 嗎?" msgid "Are you sure you want to reset the health check token?" -msgstr "" +msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥å˜å–æ†‘è‰ (access token) 嗎?" msgid "Are you sure?" -msgstr "" +msgstr "確定嗎?" msgid "Attach a file by drag & drop or %{upload_link}" msgstr "拖放檔案到æ¤è™•æˆ–者 %{upload_link}" +msgid "Authentication log" +msgstr "" + +msgid "Billing" +msgstr "" + +msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available." +msgstr "" + +msgid "BillingPlans|Current plan" +msgstr "" + +msgid "BillingPlans|Customer Support" +msgstr "" + +msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}." +msgstr "" + +msgid "BillingPlans|Manage plan" +msgstr "" + +msgid "BillingPlans|Please contact %{customer_support_link} in that case." +msgstr "" + +msgid "BillingPlans|See all %{plan_name} features" +msgstr "" + +msgid "BillingPlans|This group uses the plan associated with its parent group." +msgstr "" + +msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." +msgstr "" + +msgid "BillingPlans|Upgrade" +msgstr "" + +msgid "BillingPlans|You are currently on the %{plan_link} plan." +msgstr "" + +msgid "BillingPlans|frequently asked questions" +msgstr "" + +msgid "BillingPlans|monthly" +msgstr "" + +msgid "BillingPlans|paid annually at %{price_per_year}" +msgstr "" + +msgid "BillingPlans|per user" +msgstr "" + +msgid "Billinglans|Downgrade" +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "分支 (branch) " @@ -132,6 +204,9 @@ msgstr "ç€è¦½æª”案" msgid "ByAuthor|by" msgstr "作者:" +msgid "CI / CD" +msgstr "" + msgid "CI configuration" msgstr "CI 組態" @@ -139,7 +214,7 @@ msgid "Cancel" msgstr "å–消" msgid "Cancel edit" -msgstr "" +msgstr "å–消編輯" msgid "ChangeTypeActionLabel|Pick into branch" msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) " @@ -159,6 +234,9 @@ msgstr "更新日誌" msgid "Charts" msgstr "統計圖" +msgid "Chat" +msgstr "" + msgid "Cherry-pick this commit" msgstr "挑é¸æ¤æ›´å‹•è¨˜éŒ„ (commit) " @@ -220,7 +298,7 @@ msgid "CiStatus|running" msgstr "執行ä¸" msgid "Comments" -msgstr "" +msgstr "留言" msgid "Commit" msgid_plural "Commits" @@ -253,12 +331,18 @@ msgstr "é€äº¤è€…為 " msgid "Compare" msgstr "比較" +msgid "Container Registry" +msgstr "" + msgid "Contribution guide" msgstr "å”作指å—" msgid "Contributors" msgstr "å”作者" +msgid "Copy SSH public key to clipboard" +msgstr "" + msgid "Copy URL to clipboard" msgstr "複製網å€åˆ°å‰ªè²¼ç°¿" @@ -269,7 +353,7 @@ msgid "Create New Directory" msgstr "建立新目錄" msgid "Create a new branch" -msgstr "" +msgstr "建立新分支 (branch)" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "建立個人å˜å–æ†‘è‰ (access token) 以使用 %{protocol} 來上傳 (push) 或下載 (pull) 。" @@ -344,17 +428,20 @@ msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Deploy Keys" +msgstr "" + msgid "Description" msgstr "æè¿°" msgid "Details" -msgstr "" +msgstr "細節" msgid "Directory name" msgstr "目錄å稱" msgid "Discard changes" -msgstr "" +msgstr "放棄修改" msgid "Don't show again" msgstr "ä¸å†é¡¯ç¤º" @@ -392,23 +479,26 @@ msgstr "編輯" msgid "Edit Pipeline Schedule %{id}" msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程" -msgid "EventFilterBy|Filter by all" +msgid "Emails" msgstr "" +msgid "EventFilterBy|Filter by all" +msgstr "顯示全部" + msgid "EventFilterBy|Filter by comments" -msgstr "" +msgstr "以留言篩é¸" msgid "EventFilterBy|Filter by issue events" -msgstr "" +msgstr "以è°é¡Œ (issue) 事件篩é¸" msgid "EventFilterBy|Filter by merge events" -msgstr "" +msgstr "以åˆä½µ (merge) 事件篩é¸" msgid "EventFilterBy|Filter by push events" -msgstr "" +msgstr "ä»¥æŽ¨é€ (push) 事件篩é¸" msgid "EventFilterBy|Filter by team" -msgstr "" +msgstr "以團隊篩é¸" msgid "Every day (at 4:00am)" msgstr "æ¯æ—¥åŸ·è¡Œï¼ˆæ·©æ™¨å››é»žï¼‰" @@ -456,39 +546,51 @@ msgstr "從è°é¡Œ (issue) 建立直到部署至營é‹ç’°å¢ƒ" msgid "From merge request merge until deploy to production" msgstr "從請求被åˆä½µå¾Œ (merge request merged) 直到部署至營é‹ç’°å¢ƒ" -msgid "Git storage health information has been reset" +msgid "GPG Keys" msgstr "" -msgid "GitLab Runner section" +msgid "Geo Nodes" msgstr "" +msgid "Git storage health information has been reset" +msgstr "Git 儲å˜ç©ºé–“å¥åº·æŒ‡æ•¸å·²é‡ç½®" + +msgid "GitLab Runner section" +msgstr "GitLab Runner" + msgid "Go to your fork" msgstr "å‰å¾€æ‚¨çš„分支 (fork) " msgid "GoToYourFork|Fork" msgstr "å‰å¾€æ‚¨çš„分支 (fork) " -msgid "Health Check" +msgid "Group overview" msgstr "" +msgid "Health Check" +msgstr "å¥åº·æª¢æŸ¥" + msgid "Health information can be retrieved from the following endpoints. More information is available" -msgstr "" +msgstr "å¥åº·è³‡è¨Šå¯å¾žä»¥ä¸‹é€£çµå–得。想了解更多請åƒé–±" msgid "HealthCheck|Access token is" -msgstr "" +msgstr "å˜å–æ†‘è‰ (access token) 是" msgid "HealthCheck|Healthy" -msgstr "" +msgstr "å¥åº·" msgid "HealthCheck|No Health Problems Detected" -msgstr "" +msgstr "沒有檢測到å¥åº·å•é¡Œ" msgid "HealthCheck|Unhealthy" -msgstr "" +msgstr "ä¸è‰¯" msgid "Home" msgstr "首é " +msgid "Hooks" +msgstr "" + msgid "Housekeeping successfully started" msgstr "已開始ç¶è·" @@ -496,7 +598,7 @@ msgid "Import repository" msgstr "匯入檔案庫 (repository)" msgid "Install a Runner compatible with GitLab CI" -msgstr "" +msgstr "安è£èˆ‡ GitLab CI 相容的 Runner" msgid "Interval Pattern" msgstr "循環週期" @@ -505,16 +607,10 @@ msgid "Introducing Cycle Analytics" msgstr "週期分æžç°¡ä»‹" msgid "Issue events" -msgstr "" - -msgid "Jobs for last month" -msgstr "上個月的任務 (job) " +msgstr "è°é¡Œ (issue) 事件" -msgid "Jobs for last week" -msgstr "上個星期的任務 (job) " - -msgid "Jobs for last year" -msgstr "去年的任務 (job) " +msgid "Issues" +msgstr "" msgid "LFSStatus|Disabled" msgstr "åœç”¨" @@ -522,6 +618,9 @@ msgstr "åœç”¨" msgid "LFSStatus|Enabled" msgstr "啟用" +msgid "Labels" +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最近 %d 天" @@ -536,10 +635,10 @@ msgid "Last commit" msgstr "最後更動記錄 (commit) " msgid "LastPushEvent|You pushed to" -msgstr "" +msgstr "您上傳 (push) 了" msgid "LastPushEvent|at" -msgstr "" +msgstr "æ–¼" msgid "Learn more in the" msgstr "了解更多" @@ -553,22 +652,40 @@ msgstr "退出群組" msgid "Leave project" msgstr "退出專案" +msgid "License" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "é™åˆ¶æœ€å¤šé¡¯ç¤º %d 個事件" +msgid "Locked Files" +msgstr "" + msgid "Median" msgstr "ä¸ä½æ•¸" +msgid "Members" +msgstr "" + +msgid "Merge Requests" +msgstr "" + msgid "Merge events" +msgstr "åˆä½µ (merge) 事件" + +msgid "Messages" msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "新增 SSH 金鑰" -msgid "More information is available|here" +msgid "Monitoring" msgstr "" +msgid "More information is available|here" +msgstr "å¥åº·æª¢æŸ¥" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "建立è°é¡Œ (issue) " @@ -666,6 +783,9 @@ msgstr "åƒèˆ‡" msgid "NotificationLevel|Watch" msgstr "關注" +msgid "Notifications" +msgstr "" + msgid "OfSearchInADropdown|Filter" msgstr "篩é¸" @@ -675,9 +795,15 @@ msgstr "開始於" msgid "Options" msgstr "é¸é …" +msgid "Overview" +msgstr "" + msgid "Owner" msgstr "所有權" +msgid "Password" +msgstr "" + msgid "Pipeline" msgstr "æµæ°´ç·š (pipeline) " @@ -690,6 +816,9 @@ msgstr "æµæ°´ç·š (pipeline) 排程" msgid "Pipeline Schedules" msgstr "æµæ°´ç·š (pipeline) 排程" +msgid "Pipeline quota" +msgstr "" + msgid "PipelineCharts|Failed:" msgstr "失敗:" @@ -753,6 +882,15 @@ msgstr "æµæ°´ç·š (pipeline) " msgid "Pipelines charts" msgstr "æµæ°´ç·š (pipeline) 圖表" +msgid "Pipelines for last month" +msgstr "" + +msgid "Pipelines for last week" +msgstr "" + +msgid "Pipelines for last year" +msgstr "" + msgid "Pipeline|all" msgstr "所有" @@ -765,9 +903,15 @@ msgstr "於階段" msgid "Pipeline|with stages" msgstr "於階段" -msgid "Project" +msgid "Preferences" +msgstr "" + +msgid "Profile Settings" msgstr "" +msgid "Project" +msgstr "專案" + msgid "Project '%{project_name}' queued for deletion." msgstr "專案 '%{project_name}' å·²åŠ å…¥åˆªé™¤ä½‡åˆ—ã€‚" @@ -784,7 +928,7 @@ msgid "Project access must be granted explicitly to each user." msgstr "專案權é™å¿…é ˆä¸€ä¸€æŒ‡æ´¾çµ¦æ¯å€‹ä½¿ç”¨è€…。" msgid "Project details" -msgstr "" +msgstr "專案細節" msgid "Project export could not be deleted." msgstr "匯出的專案無法被刪除。" @@ -801,9 +945,12 @@ msgstr "專案導出已開始。完æˆå¾Œä¸‹è¼‰é€£çµæœƒé€åˆ°æ‚¨çš„信箱。" msgid "Project home" msgstr "專案首é " -msgid "ProjectActivityRSS|Subscribe" +msgid "Project overview" msgstr "" +msgid "ProjectActivityRSS|Subscribe" +msgstr "訂閱" + msgid "ProjectFeature|Disabled" msgstr "åœç”¨" @@ -825,9 +972,12 @@ msgstr "階段" msgid "ProjectNetworkGraph|Graph" msgstr "分支圖" -msgid "Push events" +msgid "Push Rules" msgstr "" +msgid "Push events" +msgstr "æŽ¨é€ (push) 事件" + msgid "Read more" msgstr "çžè§£æ›´å¤š" @@ -865,19 +1015,19 @@ msgid "Remove project" msgstr "刪除專案" msgid "Repository" -msgstr "" +msgstr "檔案庫 (repository)" msgid "Request Access" msgstr "申請權é™" msgid "Reset git storage health information" -msgstr "" +msgstr "é‡ç½® Git 儲å˜ç©ºé–“å¥åº·æŒ‡æ•¸" msgid "Reset health check access token" -msgstr "" +msgstr "é‡ç½®å¥åº·æª¢æŸ¥å˜å–æ†‘è‰ (access token)" msgid "Reset runners registration token" -msgstr "" +msgstr "é‡ç½® Runner è¨»å†Šæ†‘è‰ (registration token)" msgid "Revert this commit" msgstr "還原æ¤æ›´å‹•è¨˜éŒ„ (commit)" @@ -885,6 +1035,9 @@ msgstr "還原æ¤æ›´å‹•è¨˜éŒ„ (commit)" msgid "Revert this merge request" msgstr "還原æ¤åˆä½µè«‹æ±‚ (merge request) " +msgid "SSH Keys" +msgstr "" + msgid "Save pipeline schedule" msgstr "儲å˜æµæ°´ç·š (pipeline) 排程" @@ -904,11 +1057,14 @@ msgid "Select a timezone" msgstr "é¸æ“‡æ™‚å€" msgid "Select existing branch" -msgstr "" +msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)" msgid "Select target branch" msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) " +msgid "Service Templates" +msgstr "" + msgid "Set a password on your account to pull or push via %{protocol}." msgstr "è«‹å…ˆè¨å®šå¯†ç¢¼ï¼Œæ‰èƒ½ä½¿ç”¨ %{protocol} 來上傳 (push) 或下載 (pull) 。" @@ -924,16 +1080,25 @@ msgstr "è¨å®šè‡ªå‹•éƒ¨ç½²" msgid "SetPasswordToCloneLink|set a password" msgstr "è¨å®šå¯†ç¢¼" +msgid "Settings" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" +msgid "Snippets" +msgstr "" + msgid "Source code" msgstr "原始碼" -msgid "Specify the following URL during the Runner setup:" +msgid "Spam Logs" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "åœ¨å®‰è£ Runner 時指定以下 URL:" + msgid "StarProject|Star" msgstr "收è—" @@ -941,7 +1106,7 @@ msgid "Start a %{new_merge_request} with these changes" msgstr "以這些改動建立一個新的 %{new_merge_request} " msgid "Start the Runner!" -msgstr "" +msgstr "å•Ÿå‹• Runner!" msgid "Switch branch/tag" msgstr "切æ›åˆ†æ”¯ (branch) 或標籤" @@ -957,7 +1122,7 @@ msgid "Target Branch" msgstr "目標分支 (branch) " msgid "Team" -msgstr "" +msgstr "團隊" msgid "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." msgstr "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。" @@ -1008,7 +1173,7 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet msgstr "ä¸ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—ä¸æœ€ä¸é–“的值。例如在 3ã€5ã€9 之間,ä¸ä½æ•¸æ˜¯ 5。在 3ã€5ã€7ã€8 之間,ä¸ä½æ•¸æ˜¯ (5 + 7)/ 2 = 6。" msgid "There are problems accessing Git storage: " -msgstr "" +msgstr "å˜å– Git 儲å˜ç©ºé–“時出ç¾å•é¡Œï¼š" msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個ç¾å˜çš„檔案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•ä¸Šå‚³æ›´æ–° (push) 。" @@ -1178,7 +1343,7 @@ msgid "UploadLink|click to upload" msgstr "點擊上傳" msgid "Use the following registration token during setup:" -msgstr "" +msgstr "在安è£éŽç¨‹ä¸ä½¿ç”¨æ¤è¨»å†Šæ†‘è‰ (registration token):" msgid "Use your global notification setting" msgstr "使用全域通知è¨å®š" @@ -1204,14 +1369,17 @@ msgstr "權é™ä¸è¶³ã€‚如需查看相關資料,請å‘管ç†å“¡ç”³è«‹æ¬Šé™ã€‚ msgid "We don't have enough data to show this stage." msgstr "å› è©²éšŽæ®µçš„è³‡æ–™ä¸è¶³è€Œç„¡æ³•é¡¯ç¤ºç›¸é—œè³‡è¨Š" +msgid "Wiki" +msgstr "" + msgid "Withdraw Access Request" msgstr "å–消權é™ç”³è«‹" msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "å³å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ" +msgstr "å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ" msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" -msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。被刪除的專案無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ" +msgstr "å°‡è¦åˆªé™¤ %{project_name_with_namespace}。被刪除的專案無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgstr "å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹ %{forked_from_project} 的所有關è¯ã€‚ 真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ" @@ -1267,4 +1435,5 @@ msgstr "通知信" msgid "parent" msgid_plural "parents" -msgstr[0] "上層"
\ No newline at end of file +msgstr[0] "上層" + diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb new file mode 100644 index 00000000000..c9687af4dd2 --- /dev/null +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe IssuableCollections do + let(:user) { create(:user) } + + let(:controller) do + klass = Class.new do + def self.helper_method(name); end + + include IssuableCollections + end + + controller = klass.new + + allow(controller).to receive(:params).and_return(state: 'opened') + + controller + end + + describe '#redirect_out_of_range' do + before do + allow(controller).to receive(:url_for) + end + + it 'returns true and redirects if the offset is out of range' do + relation = double(:relation, current_page: 10) + + expect(controller).to receive(:redirect_to) + expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(true) + end + + it 'returns false if the offset is not out of range' do + relation = double(:relation, current_page: 1) + + expect(controller).not_to receive(:redirect_to) + expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(false) + end + end + + describe '#issues_page_count' do + it 'returns the number of issue pages' do + project = create(:project, :public) + + create(:issue, project: project) + + finder = IssuesFinder.new(user) + issues = finder.execute + + allow(controller).to receive(:issues_finder) + .and_return(finder) + + expect(controller.send(:issues_page_count, issues)).to eq(1) + end + end + + describe '#merge_requests_page_count' do + it 'returns the number of merge request pages' do + project = create(:project, :public) + + create(:merge_request, source_project: project, target_project: project) + + finder = MergeRequestsFinder.new(user) + merge_requests = finder.execute + + allow(controller).to receive(:merge_requests_finder) + .and_return(finder) + + pages = controller.send(:merge_requests_page_count, merge_requests) + + expect(pages).to eq(1) + end + end + + describe '#page_count_for_relation' do + it 'returns the number of pages' do + relation = double(:relation, limit_value: 20) + pages = controller.send(:page_count_for_relation, relation, 28) + + expect(pages).to eq(2) + end + end +end diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index 9d60dab12d1..b52b63e05a4 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -16,7 +16,11 @@ describe ProfilesController do end it "ignores an email update from a user with an external email address" do - ldap_user = create(:omniauth_user, external_email: true) + stub_omniauth_setting(sync_profile_from_provider: ['ldap']) + stub_omniauth_setting(sync_profile_attributes: true) + + ldap_user = create(:omniauth_user) + ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true) sign_in(ldap_user) put :update, @@ -27,5 +31,24 @@ describe ProfilesController do expect(response.status).to eq(302) expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') end + + it "ignores an email and name update but allows a location update from a user with external email and name, but not external location" do + stub_omniauth_setting(sync_profile_from_provider: ['ldap']) + stub_omniauth_setting(sync_profile_attributes: true) + + ldap_user = create(:omniauth_user, name: 'Alex') + ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true, location_synced: false) + sign_in(ldap_user) + + put :update, + user: { email: "john@gmail.com", name: "John", location: "City, Country" } + + ldap_user.reload + + expect(response.status).to eq(302) + expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') + expect(ldap_user.name).not_to eq('John') + expect(ldap_user.location).to eq('City, Country') + end end end diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index d2c613a2423..caa63e7bd22 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -81,14 +81,6 @@ describe Projects::ArtifactsController do expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) end end - - context 'when the file does not exist' do - it 'responds Not Found' do - get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' - - expect(response).to be_not_found - end - end end describe 'GET latest_succeeded' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index bb67db268fa..6775012bab5 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -56,6 +56,28 @@ describe Projects::MergeRequestsController do expect(response).to be_success end + + context "loads notes" do + let(:first_contributor) { create(:user) } + let(:contributor) { create(:user) } + let(:merge_request) { create(:merge_request, author: first_contributor, target_project: project, source_project: project) } + let(:contributor_merge_request) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) } + # the order here is important + # as the controller reloads these from DB, references doesn't correspond after + let!(:first_contributor_note) { create(:note, author: first_contributor, noteable: merge_request, project: project) } + let!(:contributor_note) { create(:note, author: contributor, noteable: merge_request, project: project) } + let!(:owner_note) { create(:note, author: user, noteable: merge_request, project: project) } + + it "with special_role FIRST_TIME_CONTRIBUTOR" do + go(format: :html) + + notes = assigns(:notes) + expect(notes).to match(a_collection_containing_exactly(an_object_having_attributes(special_role: Note::SpecialRole::FIRST_TIME_CONTRIBUTOR), + an_object_having_attributes(special_role: nil), + an_object_having_attributes(special_role: nil) + )) + end + end end describe 'as json' do diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb index a5aeffbe12d..c0beecf0bea 100644 --- a/spec/factories/gpg_signature.rb +++ b/spec/factories/gpg_signature.rb @@ -6,6 +6,6 @@ FactoryGirl.define do project gpg_key gpg_key_primary_keyid { gpg_key.primary_keyid } - valid_signature true + verification_status :verified end end diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb index 3e3404dfdac..02f50d7e27f 100644 --- a/spec/features/admin/admin_browses_logs_spec.rb +++ b/spec/features/admin/admin_browses_logs_spec.rb @@ -8,8 +8,10 @@ describe 'Admin browses logs' do it 'shows available log files' do visit admin_logs_path - expect(page).to have_content 'test.log' - expect(page).to have_content 'githost.log' - expect(page).to have_content 'application.log' + expect(page).to have_link 'application.log' + expect(page).to have_link 'githost.log' + expect(page).to have_link 'test.log' + expect(page).to have_link 'sidekiq.log' + expect(page).to have_link 'repocheck.log' end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index a6ad5981f8f..c480b5b7e34 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do let!(:label) { create(:label, project: project) } let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: label, position: 1) } - let!(:issue) { create(:issue, project: project) } - let!(:issue2) { create(:issue, project: project) } + let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') } + let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') } before do project.team << [user, :master] diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 913258ca40f..e010b5f3444 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do let!(:list2) { create(:list, board: board, label: development, position: 1) } let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } - let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) } - let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) } - let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) } - let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) } - let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) } - let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) } - let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) } - let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) } + let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } + let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } + let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } + let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } + let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } + let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } + let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } + let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } + let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } before do visit project_board_path(project, board) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 0c9fcc60d30..479fb713297 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -203,105 +203,4 @@ describe 'Commits' do end end end - - describe 'GPG signed commits', :js do - it 'changes from unverified to verified when the user changes his email to match the gpg key' do - user = create :user, email: 'unrelated.user@example.org' - project.team << [user, :master] - - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - sign_in(user) - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end - - # user changes his email which makes the gpg key verified - Sidekiq::Testing.inline! do - user.skip_reconfirmation! - user.update_attributes!(email: GpgHelpers::User1.emails.first) - end - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end - end - - it 'changes from unverified to verified when the user adds the missing gpg key' do - user = create :user, email: GpgHelpers::User1.emails.first - project.team << [user, :master] - - sign_in(user) - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end - - # user adds the gpg key which makes the signature valid - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end - end - - it 'shows popover badges' do - gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user - end - - user = create :user - project.team << [user, :master] - - sign_in(user) - visit project_commits_path(project, :'signed-commits') - - # unverified signature - click_on 'Unverified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with an unverified signature.' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" - end - - # verified and the gpg user has a gitlab profile - click_on 'Verified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content '@nannie.bernhard' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end - - # verified and the gpg user's profile doesn't exist anymore - gpg_user.destroy! - - visit project_commits_path(project, :'signed-commits') - - click_on 'Verified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content 'nannie.bernhard@example.com' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end - end - end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 06a43909053..71de9f04653 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -115,9 +115,9 @@ feature 'Dashboard Projects' do expect(page).to have_selector('.merge-request-form') expect(current_path).to eq project_new_merge_request_path(project) - expect(find('#merge_request_target_project_id').value).to eq project.id.to_s - expect(find('input#merge_request_source_branch').value).to eq 'feature' - expect(find('input#merge_request_target_branch').value).to eq 'master' + expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s + expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature' + expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master' end end end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 4297bfff3d9..2db6f9a2982 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -166,12 +166,10 @@ describe 'New/edit issue', :js do end end - page.within '.issuable-meta' do + page.within '.breadcrumbs' do issue = Issue.find_by(title: 'title') - expect(page).to have_text("Issue #{issue.to_reference}") - # compare paths because the host differ in test - expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + expect(page).to have_text("Issues #{issue.to_reference}") end end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index c470cb7c716..28b636f9359 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -40,18 +40,4 @@ feature 'Issue Detail', :js do end end end - - context 'when authored by a user who is later deleted' do - before do - issue.update_attribute(:author_id, nil) - sign_in(user) - visit project_issue_path(project, issue) - end - - it 'shows the issue' do - page.within('.issuable-details') do - expect(find('h2')).to have_content(issue.title) - end - end - end end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index e77f1f92731..ca536f2800c 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -139,7 +139,7 @@ feature 'Diff note avatars', js: true do end page.within find("[id='#{position.line_code(project.repository)}']") do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) end @@ -152,7 +152,7 @@ feature 'Diff note avatars', js: true do page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') - find('.js-comment-button').trigger 'click' + find('.js-comment-button').trigger('click') wait_for_requests end diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index ac7f75bd308..fd110e68e84 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -196,10 +196,11 @@ feature 'Diff notes resolve', js: true do end it 'does not mark discussion as resolved when resolving single note' do - page.first '.diff-content .note' do + page.within("#note_#{note.id}") do first('.line-resolve-btn').click - expect(page).to have_selector('.note-action-button .loading') + wait_for_requests + expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}") end diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 89410b0e90f..de98b147d04 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -84,13 +84,10 @@ describe 'New/edit merge request', :js do end end - page.within '.issuable-meta' do + page.within '.breadcrumbs' do merge_request = MergeRequest.find_by(source_branch: 'fix') - expect(page).to have_text("Merge request #{merge_request.to_reference}") - # compare paths because the host differ in test - expect(find_link(merge_request.to_reference)[:href]) - .to end_with(merge_request_path(merge_request)) + expect(page).to have_text("Merge Requests #{merge_request.to_reference}") end end diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb new file mode 100644 index 00000000000..55a82bdf2b9 --- /dev/null +++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Resolve outdated diff discussions', js: true do + let(:project) { create(:project, :repository, :public) } + + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'csv', target_branch: 'master') + end + + let(:outdated_diff_refs) { project.commit('926c6595b263b2a40da6b17f3e3b7ea08344fad6').diff_refs } + let(:current_diff_refs) { merge_request.diff_refs } + + let(:outdated_position) do + Gitlab::Diff::Position.new( + old_path: 'files/csv/Book1.csv', + new_path: 'files/csv/Book1.csv', + old_line: nil, + new_line: 9, + diff_refs: outdated_diff_refs + ) + end + + let(:current_position) do + Gitlab::Diff::Position.new( + old_path: 'files/csv/Book1.csv', + new_path: 'files/csv/Book1.csv', + old_line: nil, + new_line: 1, + diff_refs: current_diff_refs + ) + end + + let!(:outdated_discussion) do + create(:diff_note_on_merge_request, + project: project, + noteable: merge_request, + position: outdated_position).to_discussion + end + + let!(:current_discussion) do + create(:diff_note_on_merge_request, + noteable: merge_request, + project: project, + position: current_position).to_discussion + end + + before do + sign_in(merge_request.author) + end + + context 'when a discussion was resolved by a push' do + before do + project.update!(resolve_outdated_diff_discussions: true) + + merge_request.update_diff_discussion_positions( + old_diff_refs: outdated_diff_refs, + new_diff_refs: current_diff_refs, + current_user: merge_request.author + ) + + visit project_merge_request_path(project, merge_request) + end + + it 'shows that as automatically resolved' do + within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do + expect(page).to have_css('.discussion-body', visible: false) + expect(page).to have_content('Automatically resolved') + end + end + + it 'does not show that for active discussions' do + within(".discussion[data-discussion-id='#{current_discussion.id}']") do + expect(page).to have_css('.discussion-body', visible: true) + expect(page).not_to have_content('Automatically resolved') + end + end + end +end diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 6edc482b47e..623e4f341c5 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -42,7 +42,7 @@ feature 'Profile > GPG Keys' do scenario 'User revokes a key via the key index' do gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key - gpg_signature = create :gpg_signature, gpg_key: gpg_key, valid_signature: true + gpg_signature = create :gpg_signature, gpg_key: gpg_key, verification_status: :verified visit profile_gpg_keys_path @@ -51,7 +51,7 @@ feature 'Profile > GPG Keys' do expect(page).to have_content('Your GPG keys (0)') expect(gpg_signature.reload).to have_attributes( - valid_signature: false, + verification_status: 'unknown_key', gpg_key: nil ) end diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb index e13bf4b6089..e1852a6e544 100644 --- a/spec/features/projects/files/creating_a_file_spec.rb +++ b/spec/features/projects/files/creating_a_file_spec.rb @@ -14,7 +14,7 @@ feature 'User wants to create a file' do file_name = find('#file_name') file_name.set options[:file_name] || 'README.md' - file_content = find('#file-content') + file_content = find('#file-content', visible: false) file_content.set options[:file_content] || 'Some content' click_button 'Commit changes' diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb index b2b39dbd24c..eb2d3ff50a0 100644 --- a/spec/features/projects/sub_group_issuables_spec.rb +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -26,7 +26,6 @@ describe 'Subgroup Issuables', :js, :nested_groups do def expect_to_have_full_subgroup_title title = find('.breadcrumbs-links') - expect(title).not_to have_selector '.initializing' - expect(title).to have_content 'group / subgroup / project' + expect(title).to have_content 'group subgroup project' end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index baf3d29e6c5..81f7ab80a04 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -95,49 +95,6 @@ feature 'Project' do end end - describe 'project title' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - - before do - sign_in(user) - project.add_user(user, Gitlab::Access::MASTER) - visit project_path(project) - end - - it 'clicks toggle and shows dropdown', js: true do - find('.js-projects-dropdown-toggle').click - expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) - end - end - - describe 'project title' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - let(:project2) { create(:project, namespace: user.namespace, path: 'test') } - let(:issue) { create(:issue, project: project) } - - context 'on issues page', js: true do - before do - sign_in(user) - project.add_user(user, Gitlab::Access::MASTER) - project2.add_user(user, Gitlab::Access::MASTER) - visit project_issue_path(project, issue) - end - - it 'clicks toggle and shows dropdown' do - find('.js-projects-dropdown-toggle').click - expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2) - - page.within '.dropdown-menu-projects' do - click_link project.name_with_namespace - end - - expect(page).to have_content project.name - end - end - end - describe 'tree view (default view is set to Files)' do let(:user) { create(:user, project_view: 'files') } let(:project) { create(:forked_project_with_submodules) } diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 05a089641f1..8f6d0bb9d1b 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -295,7 +295,7 @@ describe "Search" do fill_in 'search', with: 'foo' click_button 'Search' - expect(find('#group_id').value).to eq(project.namespace.id.to_s) + expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s) end it 'preserves the project being searched in' do @@ -304,7 +304,7 @@ describe "Search" do fill_in 'search', with: 'foo' click_button 'Search' - expect(find('#project_id').value).to eq(project.id.to_s) + expect(find('#project_id', visible: false).value).to eq(project.id.to_s) end end end diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb new file mode 100644 index 00000000000..8efa5b58141 --- /dev/null +++ b/spec/features/signed_commits_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' + +describe 'GPG signed commits', :js do + let(:project) { create(:project, :repository) } + + it 'changes from unverified to verified when the user changes his email to match the gpg key' do + user = create :user, email: 'unrelated.user@example.org' + project.team << [user, :master] + + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + sign_in(user) + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).not_to have_content 'Verified' + end + + # user changes his email which makes the gpg key verified + Sidekiq::Testing.inline! do + user.skip_reconfirmation! + user.update_attributes!(email: GpgHelpers::User1.emails.first) + end + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end + end + + it 'changes from unverified to verified when the user adds the missing gpg key' do + user = create :user, email: GpgHelpers::User1.emails.first + project.team << [user, :master] + + sign_in(user) + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).not_to have_content 'Verified' + end + + # user adds the gpg key which makes the signature valid + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end + end + + context 'shows popover badges' do + let(:user_1) do + create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' + end + + let(:user_1_key) do + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user_1 + end + end + + let(:user_2) do + create(:user, email: GpgHelpers::User2.emails.first, username: 'bette.cartwright', name: 'Bette Cartwright').tap do |user| + # secondary, unverified email + create :email, user: user, email: GpgHelpers::User2.emails.last + end + end + + let(:user_2_key) do + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User2.public_key, user: user_2 + end + end + + before do + user = create :user + project.team << [user, :master] + + sign_in(user) + end + + it 'unverified signature' do + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed commit by bette cartwright')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content 'This commit was signed with an unverified signature.' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'unverified signature: user email does not match the committer email, but is the same user' do + user_2_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'unverified signature: user email does not match the committer email' do + user_2_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed commit by bette cartwright')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content "This commit was signed with a different user's verified signature." + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'verified and the gpg user has a gitlab profile' do + user_1_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + click_on 'Verified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content '@nannie.bernhard' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" + end + end + end + + it "verified and the gpg user's profile doesn't exist anymore" do + user_1_key + + visit project_commits_path(project, :'signed-commits') + + # wait for the signature to get generated + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + expect(page).to have_content 'Verified' + end + + user_1.destroy! + + refresh + + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + click_on 'Verified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content 'nannie.bernhard@example.com' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" + end + end + end + end +end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 0e80df94e18..47b173dea0a 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -15,8 +15,8 @@ describe IssuesFinder do set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } describe '#execute' do - set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } - set(:label_link) { create(:label_link, label: label, target: issue2) } + let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } @@ -347,6 +347,20 @@ describe IssuesFinder do end end + describe '#row_count', :request_store do + it 'returns the number of rows for the default state' do + finder = described_class.new(user) + + expect(finder.row_count).to eq(3) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(user, state: 'closed') + + expect(finder.row_count).to be_zero + end + end + describe '#with_confidentiality_access_check' do let(:guest) { create(:user) } set(:authorized_user) { create(:user) } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index b54155a6704..95f445e7905 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -108,4 +108,18 @@ describe MergeRequestsFinder do end end end + + describe '#row_count', :request_store do + it 'returns the number of rows for the default state' do + finder = described_class.new(user) + + expect(finder.row_count).to eq(3) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(user, state: 'closed') + + expect(finder.row_count).to eq(1) + end + end end diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb index b4368516d83..722d21c566f 100644 --- a/spec/helpers/blame_helper_spec.rb +++ b/spec/helpers/blame_helper_spec.rb @@ -35,25 +35,32 @@ describe BlameHelper do end describe '#age_map_class' do - let(:dates) do - [Time.zone.local(2014, 3, 17, 0, 0, 0)] - end - let(:blame_groups) do - [ - { commit: double(committed_date: dates[0]) } - ] - end + let(:date) { Time.zone.local(2014, 3, 17, 0, 0, 0) } + let(:blame_groups) { [{ commit: double(committed_date: date) }] } let(:duration) do - project = double(created_at: dates[0]) + project = double(created_at: date) helper.age_map_duration(blame_groups, project) end it 'returns blame-commit-age-9 when oldest' do - expect(helper.age_map_class(dates[0], duration)).to eq 'blame-commit-age-9' + expect(helper.age_map_class(date, duration)).to eq 'blame-commit-age-9' end it 'returns blame-commit-age-0 class when newest' do expect(helper.age_map_class(duration[:now], duration)).to eq 'blame-commit-age-0' end + + context 'when called on the same day as project creation' do + let(:same_day_duration) do + project = double(created_at: now) + helper.age_map_duration(today_blame_groups, project) + end + let(:today_blame_groups) { [{ commit: double(committed_date: now) }] } + let(:now) { Time.zone.now } + + it 'returns blame-commit-age-0 class' do + expect(helper.age_map_class(duration[:now], same_day_duration)).to eq 'blame-commit-age-0' + end + end end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 9d6e03e3868..05f969904f5 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -91,7 +91,8 @@ describe GroupsHelper do let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'outputs the groups in the correct order' do - expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + expect(helper.group_title(very_deep_nested_group)) + .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m) end end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 70eb01c9c44..03d706062b7 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -52,12 +52,71 @@ describe MarkupHelper do end end - describe '#link_to_gfm' do + describe '#markdown_field' do + let(:attribute) { :title } + + describe 'with already redacted attribute' do + it 'returns the redacted attribute' do + commit.redacted_title_html = 'commit title' + + expect(Banzai).not_to receive(:render_field) + + expect(helper.markdown_field(commit, attribute)).to eq('commit title') + end + end + + describe 'without redacted attribute' do + it 'renders the markdown value' do + expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original + + helper.markdown_field(commit, attribute) + end + end + end + + describe '#link_to_markdown_field' do + let(:link) { '/commits/0a1b2c3d' } + let(:issues) { create_list(:issue, 2, project: project) } + + it 'handles references nested in links with all the text' do + allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real") + + actual = helper.link_to_markdown_field(commit, :title, link) + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + expect(doc.errors).to be_empty + + # Leading commit link + expect(doc.css('a')[0].attr('href')).to eq link + expect(doc.css('a')[0].text).to eq 'This should finally fix ' + + # First issue link + expect(doc.css('a')[1].attr('href')) + .to eq project_issue_path(project, issues[0]) + expect(doc.css('a')[1].text).to eq issues[0].to_reference + + # Internal commit link + expect(doc.css('a')[2].attr('href')).to eq link + expect(doc.css('a')[2].text).to eq ' and ' + + # Second issue link + expect(doc.css('a')[3].attr('href')) + .to eq project_issue_path(project, issues[1]) + expect(doc.css('a')[3].text).to eq issues[1].to_reference + + # Trailing commit link + expect(doc.css('a')[4].attr('href')).to eq link + expect(doc.css('a')[4].text).to eq ' for real' + end + end + + describe '#link_to_markdown' do let(:link) { '/commits/0a1b2c3d' } let(:issues) { create_list(:issue, 2, project: project) } it 'handles references nested in links with all the text' do - actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link) + actual = helper.link_to_markdown("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link) doc = Nokogiri::HTML.parse(actual) # Make sure we didn't create invalid markup @@ -87,7 +146,7 @@ describe MarkupHelper do end it 'forwards HTML options' do - actual = helper.link_to_gfm("Fixed in #{commit.id}", link, class: 'foo') + actual = helper.link_to_markdown("Fixed in #{commit.id}", link, class: 'foo') doc = Nokogiri::HTML.parse(actual) expect(doc.css('a')).to satisfy do |v| @@ -98,23 +157,43 @@ describe MarkupHelper do it "escapes HTML passed in as the body" do actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" - expect(helper.link_to_gfm(actual, link)) + expect(helper.link_to_markdown(actual, link)) .to match('<h1>test</h1>') end it 'ignores reference links when they are the entire body' do text = issues[0].to_reference - act = helper.link_to_gfm(text, '/foo') + act = helper.link_to_markdown(text, '/foo') expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) end it 'replaces commit message with emoji to link' do - actual = link_to_gfm(':book: Book', '/foo') + actual = link_to_markdown(':book: Book', '/foo') expect(actual) .to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' end end + describe '#link_to_html' do + it 'wraps the rendered content in a link' do + link = '/commits/0a1b2c3d' + issue = create(:issue, project: project) + + rendered = helper.markdown("This should finally fix #{issue.to_reference} for real", pipeline: :single_line) + doc = Nokogiri::HTML.parse(rendered) + + expect(doc.css('a')[0].attr('href')) + .to eq project_issue_path(project, issue) + expect(doc.css('a')[0].text).to eq issue.to_reference + + wrapped = helper.link_to_html(rendered, link) + doc = Nokogiri::HTML.parse(wrapped) + + expect(doc.css('a')[0].attr('href')).to eq link + expect(doc.css('a')[0].text).to eq 'This should finally fix ' + end + end + describe '#render_wiki_content' do before do @wiki = double('WikiPage') diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 9921ca1af33..cd15e27b497 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -23,10 +23,10 @@ describe NotesHelper do end describe "#notes_max_access_for_users" do - it 'returns human access levels' do - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') - expect(helper.note_max_access_for_user(master_note)).to eq('Master') - expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') + it 'returns access levels' do + expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER) + expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER) + expect(helper.note_max_access_for_user(reporter_note)).to eq(Gitlab::Access::REPORTER) end it 'handles access in different projects' do @@ -34,8 +34,8 @@ describe NotesHelper do second_project.team << [master, :reporter] other_note = create(:note, author: master, project: second_project) - expect(helper.note_max_access_for_user(master_note)).to eq('Master') - expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') + expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER) + expect(helper.note_max_access_for_user(other_note)).to eq(Gitlab::Access::REPORTER) end end @@ -231,7 +231,7 @@ describe NotesHelper do end end - describe '#form_resurces' do + describe '#form_resources' do it 'returns note for personal snippet' do @snippet = create(:personal_snippet) @note = create(:note_on_personal_snippet) @@ -266,4 +266,22 @@ describe NotesHelper do expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}") end end + + describe '#discussion_resolved_intro' do + context 'when the discussion was resolved by a push' do + let(:discussion) { double(:discussion, resolved_by_push?: true) } + + it 'returns "Automatically resolved"' do + expect(discussion_resolved_intro(discussion)).to eq('Automatically resolved') + end + end + + context 'when the discussion was not resolved by a push' do + let(:discussion) { double(:discussion, resolved_by_push?: false) } + + it 'returns "Resolved"' do + expect(discussion_resolved_intro(discussion)).to eq('Resolved') + end + end + end end diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index b33b3f3a228..c1d0614c79e 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -6,22 +6,41 @@ describe ProfilesHelper do user = create(:user) allow(helper).to receive(:current_user).and_return(user) - expect(helper.email_provider_label).to be_nil + expect(helper.attribute_provider_label(:email)).to be_nil end - it "returns omniauth provider label for users with external email" do + it "returns omniauth provider label for users with external attributes" do + stub_omniauth_setting(sync_profile_from_provider: ['cas3']) + stub_omniauth_setting(sync_profile_attributes: true) stub_cas_omniauth_provider - cas_user = create(:omniauth_user, provider: 'cas3', external_email: true, email_provider: 'cas3') + cas_user = create(:omniauth_user, provider: 'cas3') + cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: true, email_synced: true, location_synced: true) allow(helper).to receive(:current_user).and_return(cas_user) - expect(helper.email_provider_label).to eq('CAS') + expect(helper.attribute_provider_label(:email)).to eq('CAS') + expect(helper.attribute_provider_label(:name)).to eq('CAS') + expect(helper.attribute_provider_label(:location)).to eq('CAS') + end + + it "returns the correct omniauth provider label for users with some external attributes" do + stub_omniauth_setting(sync_profile_from_provider: ['cas3']) + stub_omniauth_setting(sync_profile_attributes: true) + stub_cas_omniauth_provider + cas_user = create(:omniauth_user, provider: 'cas3') + cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: false, email_synced: true, location_synced: false) + allow(helper).to receive(:current_user).and_return(cas_user) + + expect(helper.attribute_provider_label(:name)).to be_nil + expect(helper.attribute_provider_label(:email)).to eq('CAS') + expect(helper.attribute_provider_label(:location)).to be_nil end it "returns 'LDAP' for users with external email but no email provider" do - ldap_user = create(:omniauth_user, external_email: true) + ldap_user = create(:omniauth_user) + ldap_user.create_user_synced_attributes_metadata(email_synced: true) allow(helper).to receive(:current_user).and_return(ldap_user) - expect(helper.email_provider_label).to eq('LDAP') + expect(helper.attribute_provider_label(:email)).to eq('LDAP') end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 463af15930d..ab647401e14 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -17,7 +17,7 @@ describe SearchHelper do end end - context "with a user" do + context "with a standard user" do let(:user) { create(:user) } before do @@ -29,7 +29,11 @@ describe SearchHelper do end it "includes default sections" do - expect(search_autocomplete_opts("adm").size).to eq(1) + expect(search_autocomplete_opts("dash").size).to eq(1) + end + + it "does not include admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(0) end it "does not allow regular expression in search term" do @@ -67,6 +71,18 @@ describe SearchHelper do end end end + + context 'with an admin user' do + let(:admin) { create(:admin) } + + before do + allow(self).to receive(:current_user).and_return(admin) + end + + it "includes admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(1) + end + end end describe 'search_filter_input_options' do diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 9523d0f4aa6..d7b66e6f078 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -3,25 +3,35 @@ require 'spec_helper' describe TreeHelper do describe 'flatten_tree' do let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } + let(:tree) { repository.tree(sha, 'files') } + let(:root_path) { 'files' } + let(:tree_item) { tree.entries.find { |entry| entry.path == path } } - before do - @repository = project.repository - @commit = project.commit("e56497bb") - end + subject { flatten_tree(root_path, tree_item) } context "on a directory containing more than one file/directory" do - let(:tree_item) { double(name: "files", path: "files") } + let(:path) { 'files/html' } it "returns the directory name" do - expect(flatten_tree(tree_item)).to match('files') + expect(subject).to match('html') end end context "on a directory containing only one directory" do - let(:tree_item) { double(name: "foo", path: "foo") } + let(:path) { 'files/flat' } it "returns the flattened path" do - expect(flatten_tree(tree_item)).to match('foo/bar') + expect(subject).to match('flat/path/correct') + end + + context "with a nested root path" do + let(:root_path) { 'files/flat' } + + it "returns the flattened path with the root path suffix removed" do + expect(subject).to match('path/correct') + end end end end diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 8c68ceff914..2aa4fb1f6c6 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -101,12 +101,13 @@ describe('Api', () => { it('fetches projects with membership when logged in', (done) => { const query = 'dummy query'; const options = { unused: 'option' }; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; window.gon.current_user_id = 1; const expectedData = Object.assign({ search: query, per_page: 20, membership: true, + simple: true, }, options); spyOn(jQuery, 'ajax').and.callFake((request) => { expect(request.url).toEqual(expectedUrl); @@ -124,10 +125,11 @@ describe('Api', () => { it('fetches projects without membership when not logged in', (done) => { const query = 'dummy query'; const options = { unused: 'option' }; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; const expectedData = Object.assign({ search: query, per_page: 20, + simple: true, }, options); spyOn(jQuery, 'ajax').and.callFake((request) => { expect(request.url).toEqual(expectedUrl); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 10fcc590c89..ca048123bf7 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -4,8 +4,11 @@ import '~/gl_dropdown'; import '~/lib/utils/common_utils'; import '~/lib/utils/url_utility'; -(() => { - const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; +describe('glDropdown', function describeDropdown() { + preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); + + const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item'; const SEARCH_INPUT_SELECTOR = '.dropdown-input-field'; const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`; @@ -39,187 +42,217 @@ import '~/lib/utils/url_utility'; remoteCallback = callback.bind({}, data); }; - describe('Dropdown', function describeDropdown() { - preloadFixtures('static/gl_dropdown.html.raw'); - loadJSONFixtures('projects.json'); - - function initDropDown(hasRemote, isFilterable, extraOpts = {}) { - const options = Object.assign({ - selectable: true, - filterable: isFilterable, - data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, - search: { - fields: ['name'] - }, - text: project => (project.name_with_namespace || project.name), - id: project => project.id, - }, extraOpts); - this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options); - } + function initDropDown(hasRemote, isFilterable, extraOpts = {}) { + const options = Object.assign({ + selectable: true, + filterable: isFilterable, + data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, + search: { + fields: ['name'] + }, + text: project => (project.name_with_namespace || project.name), + id: project => project.id, + }, extraOpts); + this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options); + } + + beforeEach(() => { + loadFixtures('static/gl_dropdown.html.raw'); + this.dropdownContainerElement = $('.dropdown.inline'); + this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); + this.projectsData = getJSONFixture('projects.json'); + }); - beforeEach(() => { - loadFixtures('static/gl_dropdown.html.raw'); - this.dropdownContainerElement = $('.dropdown.inline'); - this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); - this.projectsData = getJSONFixture('projects.json'); - }); + afterEach(() => { + $('body').unbind('keydown'); + this.dropdownContainerElement.unbind('keyup'); + }); - afterEach(() => { - $('body').unbind('keydown'); - this.dropdownContainerElement.unbind('keyup'); - }); + it('should open on click', () => { + initDropDown.call(this, false); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + this.dropdownButtonElement.click(); + expect(this.dropdownContainerElement).toHaveClass('open'); + }); - it('should open on click', () => { - initDropDown.call(this, false); - expect(this.dropdownContainerElement).not.toHaveClass('open'); - this.dropdownButtonElement.click(); - expect(this.dropdownContainerElement).toHaveClass('open'); - }); + it('escapes HTML as text', () => { + this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>'; - it('escapes HTML as text', () => { - this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>'; + initDropDown.call(this, false); - initDropDown.call(this, false); + this.dropdownButtonElement.click(); - this.dropdownButtonElement.click(); + expect( + $('.dropdown-content li:first-child').text(), + ).toBe('<script>alert("testing");</script>'); + }); - expect( - $('.dropdown-content li:first-child').text(), - ).toBe('<script>alert("testing");</script>'); - }); + it('should output HTML when highlighting', () => { + this.projectsData[0].name_with_namespace = 'testing'; + $('.dropdown-input .dropdown-input-field').val('test'); - it('should output HTML when highlighting', () => { - this.projectsData[0].name_with_namespace = 'testing'; - $('.dropdown-input .dropdown-input-field').val('test'); + initDropDown.call(this, false, true, { + highlight: true, + }); - initDropDown.call(this, false, true, { - highlight: true, - }); + this.dropdownButtonElement.click(); - this.dropdownButtonElement.click(); + expect( + $('.dropdown-content li:first-child').text(), + ).toBe('testing'); - expect( - $('.dropdown-content li:first-child').text(), - ).toBe('testing'); + expect( + $('.dropdown-content li:first-child a').html(), + ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing'); + }); - expect( - $('.dropdown-content li:first-child a').html(), - ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing'); + describe('that is open', () => { + beforeEach(() => { + initDropDown.call(this, false, false); + this.dropdownButtonElement.click(); }); - describe('that is open', () => { - beforeEach(() => { - initDropDown.call(this, false, false); - this.dropdownButtonElement.click(); + it('should select a following item on DOWN keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); + navigateWithKeys('down', randomIndex, () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); + }); - it('should select a following item on DOWN keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); - const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); - navigateWithKeys('down', randomIndex, () => { + it('should select a previous item on UP keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); + navigateWithKeys('down', (this.projectsData.length - 1), () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); + navigateWithKeys('up', randomIndex, () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); + expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); }); + }); - it('should select a previous item on UP keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); - navigateWithKeys('down', (this.projectsData.length - 1), () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); - navigateWithKeys('up', randomIndex, () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); - }); + it('should click the selected item on ENTER keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; + navigateWithKeys('down', randomIndex, () => { + spyOn(gl.utils, 'visitUrl').and.stub(); + navigateWithKeys('enter', null, () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); + expect(link).toHaveClass('is-active'); + const linkedLocation = link.attr('href'); + if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); + }); - it('should click the selected item on ENTER keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); - const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; - navigateWithKeys('down', randomIndex, () => { - spyOn(gl.utils, 'visitUrl').and.stub(); - navigateWithKeys('enter', null, () => { - expect(this.dropdownContainerElement).not.toHaveClass('open'); - const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); - expect(link).toHaveClass('is-active'); - const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); - }); - }); + it('should close on ESC keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC }); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + }); + }); - it('should close on ESC keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); - this.dropdownContainerElement.trigger({ - type: 'keyup', - which: ARROW_KEYS.ESC, - keyCode: ARROW_KEYS.ESC - }); - expect(this.dropdownContainerElement).not.toHaveClass('open'); + describe('opened and waiting for a remote callback', () => { + beforeEach(() => { + initDropDown.call(this, true, true); + this.dropdownButtonElement.click(); + }); + + it('should show loading indicator while search results are being fetched by backend', () => { + const dropdownMenu = document.querySelector('.dropdown-menu'); + + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + remoteCallback(); + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + }); + + it('should not focus search input while remote task is not complete', () => { + expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus search input after remote task is complete', () => { + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus on input when opening for the second time after transition', () => { + remoteCallback(); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC }); + this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); + }); + + describe('input focus with array data', () => { + it('should focus input when passing array data to drop down', () => { + initDropDown.call(this, false, true); + this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + }); + + it('should still have input value on close and restore', () => { + const $searchInput = $(SEARCH_INPUT_SELECTOR); + initDropDown.call(this, false, true); + $searchInput + .trigger('focus') + .val('g') + .trigger('input'); + expect($searchInput.val()).toEqual('g'); + this.dropdownButtonElement.trigger('hidden.bs.dropdown'); + $searchInput + .trigger('blur') + .trigger('focus'); + expect($searchInput.val()).toEqual('g'); + }); + + describe('renderItem', () => { + describe('without selected value', () => { + let dropdown; - describe('opened and waiting for a remote callback', () => { beforeEach(() => { - initDropDown.call(this, true, true); - this.dropdownButtonElement.click(); + const dropdownOptions = { + + }; + const $dropdownDiv = $('<div />'); + $dropdownDiv.glDropdown(dropdownOptions); + dropdown = $dropdownDiv.data('glDropdown'); }); - it('should show loading indicator while search results are being fetched by backend', () => { - const dropdownMenu = document.querySelector('.dropdown-menu'); + it('marks items without ID as active', () => { + const dummyData = { }; - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); - remoteCallback(); - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); - }); + const html = dropdown.renderItem(dummyData, null, null); - it('should not focus search input while remote task is not complete', () => { - expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); - remoteCallback(); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + const link = html.querySelector('a'); + expect(link).toHaveClass('is-active'); }); - it('should focus search input after remote task is complete', () => { - remoteCallback(); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); - }); + it('does not mark items with ID as active', () => { + const dummyData = { + id: 'ea' + }; - it('should focus on input when opening for the second time after transition', () => { - remoteCallback(); - this.dropdownContainerElement.trigger({ - type: 'keyup', - which: ARROW_KEYS.ESC, - keyCode: ARROW_KEYS.ESC - }); - this.dropdownButtonElement.click(); - this.dropdownContainerElement.trigger('transitionend'); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); - }); - }); + const html = dropdown.renderItem(dummyData, null, null); - describe('input focus with array data', () => { - it('should focus input when passing array data to drop down', () => { - initDropDown.call(this, false, true); - this.dropdownButtonElement.click(); - this.dropdownContainerElement.trigger('transitionend'); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + const link = html.querySelector('a'); + expect(link).not.toHaveClass('is-active'); }); }); - - it('should still have input value on close and restore', () => { - const $searchInput = $(SEARCH_INPUT_SELECTOR); - initDropDown.call(this, false, true); - $searchInput - .trigger('focus') - .val('g') - .trigger('input'); - expect($searchInput.val()).toEqual('g'); - this.dropdownButtonElement.trigger('hidden.bs.dropdown'); - $searchInput - .trigger('blur') - .trigger('focus'); - expect($searchInput.val()).toEqual('g'); - }); }); -})(); +}); diff --git a/spec/javascripts/monitoring/graph_row_spec.js b/spec/javascripts/monitoring/graph_row_spec.js deleted file mode 100644 index 6a79d7c8f82..00000000000 --- a/spec/javascripts/monitoring/graph_row_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import Vue from 'vue'; -import GraphRow from '~/monitoring/components/graph_row.vue'; -import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; -import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data'; - -const createComponent = (propsData) => { - const Component = Vue.extend(GraphRow); - - return new Component({ - propsData, - }).$mount(); -}; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -describe('GraphRow', () => { - beforeEach(() => { - spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); - }); - describe('Computed props', () => { - it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { - const component = createComponent({ - rowData: convertedMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.bootstrapClass).toEqual('col-md-6'); - }); - - it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => { - const component = createComponent({ - rowData: [convertedMetrics[0]], - updateAspectRatio: false, - deploymentData, - }); - - expect(component.bootstrapClass).toEqual('col-md-12'); - }); - }); - - it('has one column', () => { - const component = createComponent({ - rowData: convertedMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.$el.querySelectorAll('.prometheus-svg-container').length) - .toEqual(component.rowData.length); - }); - - it('has two columns', () => { - const component = createComponent({ - rowData: convertedMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.$el.querySelectorAll('.col-md-6').length) - .toEqual(component.rowData.length); - }); -}); diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js index 20c1e6a0005..88aa7659275 100644 --- a/spec/javascripts/monitoring/monitoring_store_spec.js +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -5,10 +5,10 @@ describe('MonitoringStore', () => { this.store = new MonitoringStore(); this.store.storeMetrics(MonitoringMock.data); - it('contains one group that contains two queries sorted by priority in one row', () => { + it('contains one group that contains two queries sorted by priority', () => { expect(this.store.groups).toBeDefined(); expect(this.store.groups.length).toEqual(1); - expect(this.store.groups[0].metrics.length).toEqual(1); + expect(this.store.groups[0].metrics.length).toEqual(2); }); it('gets the metrics count for every group', () => { diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js deleted file mode 100644 index 3d36bb3e4d4..00000000000 --- a/spec/javascripts/project_title_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -/* global Project */ - -import 'select2/select2'; -import '~/gl_dropdown'; -import '~/api'; -import '~/project_select'; -import '~/project'; - -describe('Project Title', () => { - const dummyApiVersion = 'v3000'; - preloadFixtures('issues/open-issue.html.raw'); - loadJSONFixtures('projects.json'); - - beforeEach(() => { - loadFixtures('issues/open-issue.html.raw'); - - window.gon = {}; - window.gon.api_version = dummyApiVersion; - - // eslint-disable-next-line no-new - new Project(); - }); - - describe('project list', () => { - let reqUrl; - let reqData; - - beforeEach(() => { - const fakeResponseData = getJSONFixture('projects.json'); - spyOn(jQuery, 'ajax').and.callFake((req) => { - const def = $.Deferred(); - reqUrl = req.url; - reqData = req.data; - def.resolve(fakeResponseData); - return def.promise(); - }); - }); - - it('toggles dropdown', () => { - const $menu = $('.js-dropdown-menu-projects'); - window.gon.current_user_id = 1; - $('.js-projects-dropdown-toggle').click(); - expect($menu).toHaveClass('open'); - expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`); - expect(reqData).toEqual({ - search: '', - order_by: 'last_activity_at', - per_page: 20, - membership: true, - }); - $menu.find('.dropdown-menu-close-icon').click(); - expect($menu).not.toHaveClass('open'); - }); - }); - - afterEach(() => { - window.gon = {}; - }); -}); diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js new file mode 100644 index 00000000000..42f0f6fc1af --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/app_spec.js @@ -0,0 +1,348 @@ +import Vue from 'vue'; + +import bp from '~/breakpoints'; +import appComponent from '~/projects_dropdown/components/app.vue'; +import eventHub from '~/projects_dropdown/event_hub'; +import ProjectsStore from '~/projects_dropdown/store/projects_store'; +import ProjectsService from '~/projects_dropdown/service/projects_service'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { currentSession, mockProject, mockRawProject } from '../mock_data'; + +const createComponent = () => { + gon.api_version = currentSession.apiVersion; + const Component = Vue.extend(appComponent); + const store = new ProjectsStore(); + const service = new ProjectsService(currentSession.username); + + return mountComponent(Component, { + store, + service, + currentUserName: currentSession.username, + currentProject: currentSession.project, + }); +}; + +const returnServicePromise = (data, failed) => new Promise((resolve, reject) => { + if (failed) { + reject(data); + } else { + resolve({ + json() { + return data; + }, + }); + } +}); + +describe('AppComponent', () => { + describe('computed', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('frequentProjects', () => { + it('should return list of frequently accessed projects from store', () => { + expect(vm.frequentProjects).toBeDefined(); + expect(vm.frequentProjects.length).toBe(0); + + vm.store.setFrequentProjects([mockProject]); + expect(vm.frequentProjects).toBeDefined(); + expect(vm.frequentProjects.length).toBe(1); + }); + }); + + describe('searchProjects', () => { + it('should return list of frequently accessed projects from store', () => { + expect(vm.searchProjects).toBeDefined(); + expect(vm.searchProjects.length).toBe(0); + + vm.store.setSearchedProjects([mockRawProject]); + expect(vm.searchProjects).toBeDefined(); + expect(vm.searchProjects.length).toBe(1); + }); + }); + }); + + describe('methods', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggleFrequentProjectsList', () => { + it('should toggle props which control visibility of Frequent Projects list from state passed', () => { + vm.toggleFrequentProjectsList(true); + expect(vm.isLoadingProjects).toBeFalsy(); + expect(vm.isSearchListVisible).toBeFalsy(); + expect(vm.isFrequentsListVisible).toBeTruthy(); + + vm.toggleFrequentProjectsList(false); + expect(vm.isLoadingProjects).toBeTruthy(); + expect(vm.isSearchListVisible).toBeTruthy(); + expect(vm.isFrequentsListVisible).toBeFalsy(); + }); + }); + + describe('toggleSearchProjectsList', () => { + it('should toggle props which control visibility of Searched Projects list from state passed', () => { + vm.toggleSearchProjectsList(true); + expect(vm.isLoadingProjects).toBeFalsy(); + expect(vm.isFrequentsListVisible).toBeFalsy(); + expect(vm.isSearchListVisible).toBeTruthy(); + + vm.toggleSearchProjectsList(false); + expect(vm.isLoadingProjects).toBeTruthy(); + expect(vm.isFrequentsListVisible).toBeTruthy(); + expect(vm.isSearchListVisible).toBeFalsy(); + }); + }); + + describe('toggleLoader', () => { + it('should toggle props which control visibility of list loading animation from state passed', () => { + vm.toggleLoader(true); + expect(vm.isFrequentsListVisible).toBeFalsy(); + expect(vm.isSearchListVisible).toBeFalsy(); + expect(vm.isLoadingProjects).toBeTruthy(); + + vm.toggleLoader(false); + expect(vm.isFrequentsListVisible).toBeTruthy(); + expect(vm.isSearchListVisible).toBeTruthy(); + expect(vm.isLoadingProjects).toBeFalsy(); + }); + }); + + describe('fetchFrequentProjects', () => { + it('should set props for loading animation to `true` while frequent projects list is being loaded', () => { + spyOn(vm, 'toggleLoader'); + + vm.fetchFrequentProjects(); + expect(vm.isLocalStorageFailed).toBeFalsy(); + expect(vm.toggleLoader).toHaveBeenCalledWith(true); + }); + + it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => { + const mockData = [mockProject]; + + spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData); + spyOn(vm.store, 'setFrequentProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData); + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + }); + + it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => { + spyOn(vm.service, 'getFrequentProjects').and.returnValue(null); + spyOn(vm.store, 'setFrequentProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + expect(vm.isLocalStorageFailed).toBeFalsy(); + + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]); + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + expect(vm.isLocalStorageFailed).toBeTruthy(); + }); + + it('should set props for search results list to `true` if search query was already made previously', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + spyOn(vm.service, 'getFrequentProjects'); + spyOn(vm, 'toggleSearchProjectsList'); + + vm.searchQuery = 'test'; + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).not.toHaveBeenCalled(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + }); + + it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getFrequentProjects'); + + vm.searchQuery = 'test'; + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled(); + }); + }); + + describe('fetchSearchedProjects', () => { + const searchQuery = 'test'; + + it('should perform search with provided search query', (done) => { + const mockData = [mockRawProject]; + spyOn(vm, 'toggleLoader'); + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData)); + spyOn(vm.store, 'setSearchedProjects'); + + vm.fetchSearchedProjects(searchQuery); + setTimeout(() => { + expect(vm.searchQuery).toBe(searchQuery); + expect(vm.toggleLoader).toHaveBeenCalledWith(true); + expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData); + done(); + }, 0); + }); + + it('should update props for showing search failure', (done) => { + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true)); + + vm.fetchSearchedProjects(searchQuery); + setTimeout(() => { + expect(vm.searchQuery).toBe(searchQuery); + expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery); + expect(vm.isSearchFailed).toBeTruthy(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + done(); + }, 0); + }); + }); + + describe('logCurrentProjectAccess', () => { + it('should log current project access via service', (done) => { + spyOn(vm.service, 'logProjectAccess'); + + vm.currentProject = mockProject; + vm.logCurrentProjectAccess(); + + setTimeout(() => { + expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject); + done(); + }, 1); + }); + }); + + describe('handleSearchClear', () => { + it('should show frequent projects list when search input is cleared', () => { + spyOn(vm.store, 'clearSearchedProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + vm.handleSearchClear(); + + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + expect(vm.store.clearSearchedProjects).toHaveBeenCalled(); + expect(vm.searchQuery).toBe(''); + }); + }); + + describe('handleSearchFailure', () => { + it('should show failure message within dropdown', () => { + spyOn(vm, 'toggleSearchProjectsList'); + + vm.handleSearchFailure(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + expect(vm.isSearchFailed).toBeTruthy(); + }); + }); + }); + + describe('created', () => { + it('should bind event listeners on eventHub', (done) => { + spyOn(eventHub, '$on'); + + createComponent().$mount(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$off'); + + vm.$mount(); + vm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render search input', () => { + expect(vm.$el.querySelector('.search-input-container')).toBeDefined(); + }); + + it('should render loading animation', (done) => { + vm.toggleLoader(true); + Vue.nextTick(() => { + const loadingEl = vm.$el.querySelector('.loading-animation'); + + expect(loadingEl).toBeDefined(); + expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy(); + expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects'); + done(); + }); + }); + + it('should render frequent projects list header', (done) => { + vm.toggleFrequentProjectsList(true); + Vue.nextTick(() => { + const sectionHeaderEl = vm.$el.querySelector('.section-header'); + + expect(sectionHeaderEl).toBeDefined(); + expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited'); + done(); + }); + }); + + it('should render frequent projects list', (done) => { + vm.toggleFrequentProjectsList(true); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined(); + done(); + }); + }); + + it('should render searched projects list', (done) => { + vm.toggleSearchProjectsList(true); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.section-header')).toBe(null); + expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js new file mode 100644 index 00000000000..fcd0f6a3630 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; + +import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockFrequents } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListFrequentComponent); + + return mountComponent(Component, { + projects: mockFrequents, + localStorageFailed: false, + }); +}; + +describe('ProjectsListFrequentComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isListEmpty', () => { + it('should return `true` or `false` representing whether if `projects` is empty of not', () => { + vm.projects = []; + expect(vm.isListEmpty).toBeTruthy(); + + vm.projects = mockFrequents; + expect(vm.isListEmpty).toBeFalsy(); + }); + }); + + describe('listEmptyMessage', () => { + it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => { + vm.localStorageFailed = true; + expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support'); + + vm.localStorageFailed = false; + expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here'); + }); + }); + }); + + describe('template', () => { + it('should render component element with list of projects', (done) => { + vm.projects = mockFrequents; + + Vue.nextTick(() => { + expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5); + done(); + }); + }); + + it('should render component element with empty message', (done) => { + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js new file mode 100644 index 00000000000..171629fcd6b --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; + +import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockProject } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListItemComponent); + + return mountComponent(Component, { + projectId: mockProject.id, + projectName: mockProject.name, + namespace: mockProject.namespace, + webUrl: mockProject.webUrl, + avatarUrl: mockProject.avatarUrl, + }); +}; + +describe('ProjectsListItemComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hasAvatar', () => { + it('should return `true` or `false` if whether avatar is present or not', () => { + vm.avatarUrl = 'path/to/avatar.png'; + expect(vm.hasAvatar).toBeTruthy(); + + vm.avatarUrl = null; + expect(vm.hasAvatar).toBeFalsy(); + }); + }); + + describe('highlightedProjectName', () => { + it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => { + vm.matcher = 'lab'; + expect(vm.highlightedProjectName).toContain('<b>Lab</b>'); + }); + + it('should return project name as it is if `matcher` is not available', () => { + vm.matcher = null; + expect(vm.highlightedProjectName).toBe(mockProject.name); + }); + }); + }); + + describe('template', () => { + it('should render component element', () => { + expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('a').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-title').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js new file mode 100644 index 00000000000..59fc2dedba5 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockProject } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListSearchComponent); + + return mountComponent(Component, { + projects: [mockProject], + matcher: 'lab', + searchFailed: false, + }); +}; + +describe('ProjectsListSearchComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isListEmpty', () => { + it('should return `true` or `false` representing whether if `projects` is empty of not', () => { + vm.projects = []; + expect(vm.isListEmpty).toBeTruthy(); + + vm.projects = [mockProject]; + expect(vm.isListEmpty).toBeFalsy(); + }); + }); + + describe('listEmptyMessage', () => { + it('should return appropriate empty list message based on value of `searchFailed` prop', () => { + vm.searchFailed = true; + expect(vm.listEmptyMessage).toBe('Something went wrong on our end.'); + + vm.searchFailed = false; + expect(vm.listEmptyMessage).toBe('No projects matched your query'); + }); + }); + }); + + describe('template', () => { + it('should render component element with list of projects', (done) => { + vm.projects = [mockProject]; + + Vue.nextTick(() => { + expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1); + done(); + }); + }); + + it('should render component element with empty message', (done) => { + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + + it('should render component element with failure message', (done) => { + vm.searchFailed = true; + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js new file mode 100644 index 00000000000..f2a23e33325 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/search_spec.js @@ -0,0 +1,101 @@ +import Vue from 'vue'; + +import searchComponent from '~/projects_dropdown/components/search.vue'; +import eventHub from '~/projects_dropdown/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(searchComponent); + + return mountComponent(Component); +}; + +describe('SearchComponent', () => { + describe('methods', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('setFocus', () => { + it('should set focus to search input', () => { + spyOn(vm.$refs.search, 'focus'); + + vm.setFocus(); + expect(vm.$refs.search.focus).toHaveBeenCalled(); + }); + }); + + describe('emitSearchEvents', () => { + it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => { + const searchQuery = 'test'; + spyOn(eventHub, '$emit'); + vm.searchQuery = searchQuery; + vm.emitSearchEvents(); + expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery); + }); + + it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => { + spyOn(eventHub, '$emit'); + vm.searchQuery = ''; + vm.emitSearchEvents(); + expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared'); + }); + }); + }); + + describe('mounted', () => { + it('should listen `dropdownOpen` event', (done) => { + spyOn(eventHub, '$on'); + createComponent(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$off'); + + vm.$mount(); + vm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render component element', () => { + const inputEl = vm.$el.querySelector('input.form-control'); + + expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); + expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy(); + expect(inputEl).not.toBe(null); + expect(inputEl.getAttribute('placeholder')).toBe('Search projects'); + expect(vm.$el.querySelector('.search-icon')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js new file mode 100644 index 00000000000..d6a79fb8ac1 --- /dev/null +++ b/spec/javascripts/projects_dropdown/mock_data.js @@ -0,0 +1,96 @@ +export const currentSession = { + username: 'root', + storageKey: 'root/frequent-projects', + apiVersion: 'v4', + project: { + id: 1, + name: 'dummy-project', + namespace: 'SamepleGroup / Dummy-Project', + webUrl: 'http://127.0.0.1/samplegroup/dummy-project', + avatarUrl: null, + lastAccessedOn: Date.now(), + }, +}; + +export const mockProject = { + id: 1, + name: 'GitLab Community Edition', + namespace: 'gitlab-org / gitlab-ce', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatarUrl: null, +}; + +export const mockRawProject = { + id: 1, + name: 'GitLab Community Edition', + name_with_namespace: 'gitlab-org / gitlab-ce', + web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatar_url: null, +}; + +export const mockFrequents = [ + { + id: 1, + name: 'GitLab Community Edition', + namespace: 'gitlab-org / gitlab-ce', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatarUrl: null, + }, + { + id: 2, + name: 'GitLab CI', + namespace: 'gitlab-org / gitlab-ci', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci', + avatarUrl: null, + }, + { + id: 3, + name: 'Typeahead.Js', + namespace: 'twitter / typeahead-js', + webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js', + avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png', + }, + { + id: 4, + name: 'Intel', + namespace: 'platform / hardware / bsp / intel', + webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel', + avatarUrl: null, + }, + { + id: 5, + name: 'v4.4', + namespace: 'platform / hardware / bsp / kernel / common / v4.4', + webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4', + avatarUrl: null, + }, +]; + +export const unsortedFrequents = [ + { id: 1, frequency: 12, lastAccessedOn: 1491400843391 }, + { id: 2, frequency: 14, lastAccessedOn: 1488240890738 }, + { id: 3, frequency: 44, lastAccessedOn: 1497675908472 }, + { id: 4, frequency: 8, lastAccessedOn: 1497979281815 }, + { id: 5, frequency: 34, lastAccessedOn: 1488089211943 }, + { id: 6, frequency: 14, lastAccessedOn: 1493517292488 }, + { id: 7, frequency: 42, lastAccessedOn: 1486815299875 }, + { id: 8, frequency: 33, lastAccessedOn: 1500762279114 }, + { id: 10, frequency: 46, lastAccessedOn: 1483251641543 }, +]; + +/** + * This const has a specific order which tests authenticity + * of `ProjectsService.getTopFrequentProjects` method so + * DO NOT change order of items in this const. + */ +export const sortedFrequents = [ + { id: 10, frequency: 46, lastAccessedOn: 1483251641543 }, + { id: 3, frequency: 44, lastAccessedOn: 1497675908472 }, + { id: 7, frequency: 42, lastAccessedOn: 1486815299875 }, + { id: 5, frequency: 34, lastAccessedOn: 1488089211943 }, + { id: 8, frequency: 33, lastAccessedOn: 1500762279114 }, + { id: 6, frequency: 14, lastAccessedOn: 1493517292488 }, + { id: 2, frequency: 14, lastAccessedOn: 1488240890738 }, + { id: 1, frequency: 12, lastAccessedOn: 1491400843391 }, + { id: 4, frequency: 8, lastAccessedOn: 1497979281815 }, +]; diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js new file mode 100644 index 00000000000..d5dd8b3449a --- /dev/null +++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js @@ -0,0 +1,179 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import bp from '~/breakpoints'; +import ProjectsService from '~/projects_dropdown/service/projects_service'; +import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants'; +import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data'; + +Vue.use(VueResource); + +FREQUENT_PROJECTS.MAX_COUNT = 3; + +describe('ProjectsService', () => { + let service; + + beforeEach(() => { + gon.api_version = currentSession.apiVersion; + gon.current_user_id = 1; + service = new ProjectsService(currentSession.username); + }); + + describe('contructor', () => { + it('should initialize default properties of class', () => { + expect(service.isLocalStorageAvailable).toBeTruthy(); + expect(service.currentUserName).toBe(currentSession.username); + expect(service.storageKey).toBe(currentSession.storageKey); + expect(service.projectsPath).toBeDefined(); + }); + }); + + describe('getSearchedProjects', () => { + it('should return promise from VueResource HTTP GET', () => { + spyOn(service.projectsPath, 'get').and.stub(); + + const searchQuery = 'lab'; + const queryParams = { + simple: false, + per_page: 20, + membership: true, + order_by: 'last_activity_at', + search: searchQuery, + }; + + service.getSearchedProjects(searchQuery); + expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams); + }); + }); + + describe('logProjectAccess', () => { + let storage; + + beforeEach(() => { + storage = {}; + + spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => { + storage[storageKey] = value; + }); + + spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => { + if (storage[storageKey]) { + return storage[storageKey]; + } + + return null; + }); + }); + + it('should create a project store if it does not exist and adds a project', () => { + service.logProjectAccess(currentSession.project); + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(1); + expect(projects[0].frequency).toBe(1); + expect(projects[0].lastAccessedOn).toBeDefined(); + }); + + it('should prevent inserting same report multiple times into store', () => { + service.logProjectAccess(currentSession.project); + service.logProjectAccess(currentSession.project); + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(1); + }); + + it('should increase frequency of report if it was logged multiple times over the course of an hour', () => { + let projects; + spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1; + service.logProjectAccess(currentSession.project); + + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].frequency).toBe(1); + + service.logProjectAccess(currentSession.project); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].frequency).toBe(2); + expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn); + }); + + it('should always update project metadata', () => { + let projects; + const oldProject = { + ...currentSession.project, + }; + + const newProject = { + ...currentSession.project, + name: 'New Name', + avatarUrl: 'new/avatar.png', + namespace: 'New / Namespace', + webUrl: 'http://localhost/new/web/url', + }; + + service.logProjectAccess(oldProject); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].name).toBe(oldProject.name); + expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl); + expect(projects[0].namespace).toBe(oldProject.namespace); + expect(projects[0].webUrl).toBe(oldProject.webUrl); + + service.logProjectAccess(newProject); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].name).toBe(newProject.name); + expect(projects[0].avatarUrl).toBe(newProject.avatarUrl); + expect(projects[0].namespace).toBe(newProject.namespace); + expect(projects[0].webUrl).toBe(newProject.webUrl); + }); + + it('should not add more than 20 projects in store', () => { + for (let i = 1; i <= 5; i += 1) { + const project = Object.assign(currentSession.project, { id: i }); + service.logProjectAccess(project); + } + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(3); + }); + }); + + describe('getTopFrequentProjects', () => { + let storage = {}; + + beforeEach(() => { + storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents); + + spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => { + if (storage[storageKey]) { + return storage[storageKey]; + } + + return null; + }); + }); + + it('should return top 5 frequently accessed projects for desktop screens', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + const frequentProjects = service.getTopFrequentProjects(); + + expect(frequentProjects.length).toBe(5); + frequentProjects.forEach((project, index) => { + expect(project.id).toBe(sortedFrequents[index].id); + }); + }); + + it('should return top 3 frequently accessed projects for mobile screens', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + const frequentProjects = service.getTopFrequentProjects(); + + expect(frequentProjects.length).toBe(3); + frequentProjects.forEach((project, index) => { + expect(project.id).toBe(sortedFrequents[index].id); + }); + }); + + it('should return empty array if there are no projects available in store', () => { + storage = {}; + expect(service.getTopFrequentProjects().length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js new file mode 100644 index 00000000000..e57399d37cd --- /dev/null +++ b/spec/javascripts/projects_dropdown/store/projects_store_spec.js @@ -0,0 +1,41 @@ +import ProjectsStore from '~/projects_dropdown/store/projects_store'; +import { mockProject, mockRawProject } from '../mock_data'; + +describe('ProjectsStore', () => { + let store; + + beforeEach(() => { + store = new ProjectsStore(); + }); + + describe('setFrequentProjects', () => { + it('should set frequent projects list to state', () => { + store.setFrequentProjects([mockProject]); + + expect(store.getFrequentProjects().length).toBe(1); + expect(store.getFrequentProjects()[0].id).toBe(mockProject.id); + }); + }); + + describe('setSearchedProjects', () => { + it('should set searched projects list to state', () => { + store.setSearchedProjects([mockRawProject]); + + const processedProjects = store.getSearchedProjects(); + expect(processedProjects.length).toBe(1); + expect(processedProjects[0].id).toBe(mockRawProject.id); + expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace); + expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url); + expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url); + }); + }); + + describe('clearSearchedProjects', () => { + it('should clear searched projects list from state', () => { + store.setSearchedProjects([mockRawProject]); + expect(store.getSearchedProjects().length).toBe(1); + store.clearSearchedProjects(); + expect(store.getSearchedProjects().length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/identicon_spec.js b/spec/javascripts/vue_shared/components/identicon_spec.js index 4f194e5a64e..647680f00f7 100644 --- a/spec/javascripts/vue_shared/components/identicon_spec.js +++ b/spec/javascripts/vue_shared/components/identicon_spec.js @@ -1,25 +1,30 @@ import Vue from 'vue'; import identiconComponent from '~/vue_shared/components/identicon.vue'; -const createComponent = () => { +const createComponent = (sizeClass) => { const Component = Vue.extend(identiconComponent); return new Component({ propsData: { entityId: 1, entityName: 'entity-name', + sizeClass, }, }).$mount(); }; describe('IdenticonComponent', () => { - let vm; + describe('computed', () => { + let vm; - beforeEach(() => { - vm = createComponent(); - }); + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); - describe('computed', () => { describe('identiconStyles', () => { it('should return styles attribute value with `background-color` property', () => { vm.entityId = 4; @@ -48,9 +53,20 @@ describe('IdenticonComponent', () => { describe('template', () => { it('should render identicon', () => { + const vm = createComponent(); + expect(vm.$el.nodeName).toBe('DIV'); expect(vm.$el.classList.contains('identicon')).toBeTruthy(); + expect(vm.$el.classList.contains('s40')).toBeTruthy(); expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy(); + vm.$destroy(); + }); + + it('should render identicon with provided sizing class', () => { + const vm = createComponent('s32'); + + expect(vm.$el.classList.contains('s32')).toBeTruthy(); + vm.$destroy(); }); }); }); diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb new file mode 100644 index 00000000000..049d025a5b9 --- /dev/null +++ b/spec/lib/banzai/commit_renderer_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Banzai::CommitRenderer do + describe '.render' do + it 'renders a commit description and title' do + user = double(:user) + project = create(:project, :repository) + + expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original + + described_class::ATTRIBUTES.each do |attr| + expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr) + end + + described_class.render([project.commit], project, user) + end + end +end diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index ff6b19459bb..85eddde732e 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do expect(links.last.attr('href')).to eq '#header-2' expect(links.last.text).to eq 'Header 2' end + + context 'table of contents nesting' do + let(:results) do + result( + header(1, 'Header 1') << + header(2, 'Header 1-1') << + header(3, 'Header 1-1-1') << + header(2, 'Header 1-2') << + header(1, 'Header 2') << + header(2, 'Header 2-1') + ) + end + + it 'keeps list levels regarding header levels' do + items = doc.css('li') + + # Header 1 + expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 1-1 + expect(items[1].ancestors).to include(items[0]) + + # Header 1-1-1 + expect(items[2].ancestors).to include(items[0], items[1]) + + # Header 1-2 + expect(items[3].ancestors).to include(items[0]) + expect(items[3].ancestors).not_to include(items[1]) + + # Header 2 + expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 2-1 + expect(items[5].ancestors).to include(items[4]) + end + end end end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 7f5d481c36c..b172a1b718c 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -1,53 +1,77 @@ require 'spec_helper' describe Banzai::ObjectRenderer do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:renderer) { described_class.new(project, user, custom_value: 'value') } let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } describe '#render' do - it 'renders and redacts an Array of objects' do - renderer.render([object], :note) + context 'with cache' do + it 'renders and redacts an Array of objects' do + renderer.render([object], :note) - expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' - expect(object.user_visible_reference_count).to eq 0 - end + expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' + expect(object.user_visible_reference_count).to eq 0 + end - it 'calls Banzai::Redactor to perform redaction' do - expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + it 'calls Banzai::Redactor to perform redaction' do + expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original - renderer.render([object], :note) - end + renderer.render([object], :note) + end - it 'retrieves field content using Banzai.render_field' do - expect(Banzai).to receive(:render_field).with(object, :note).and_call_original + it 'retrieves field content using Banzai::Renderer.render_field' do + expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original - renderer.render([object], :note) - end + renderer.render([object], :note) + end - it 'passes context to PostProcessPipeline' do - another_user = create(:user) - another_project = create(:project) - object = Note.new( - note: 'hello', - note_html: 'hello', - author: another_user, - project: another_project - ) - - expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with( - anything, - hash_including( - skip_redaction: true, - current_user: user, - project: another_project, + it 'passes context to PostProcessPipeline' do + another_user = create(:user) + another_project = create(:project) + object = Note.new( + note: 'hello', + note_html: 'hello', author: another_user, - custom_value: 'value' + project: another_project ) - ).and_call_original - renderer.render([object], :note) + expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with( + anything, + hash_including( + skip_redaction: true, + current_user: user, + project: another_project, + author: another_user, + custom_value: 'value' + ) + ).and_call_original + + renderer.render([object], :note) + end + end + + context 'without cache' do + let(:commit) { project.commit } + + it 'renders and redacts an Array of objects' do + renderer.render([commit], :title) + + expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'") + end + + it 'calls Banzai::Redactor to perform redaction' do + expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + + renderer.render([commit], :title) + end + + it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original + + renderer.render([commit], :title) + end end end end diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index 0e094405e33..da42272bbef 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -4,6 +4,7 @@ describe Banzai::Renderer do def fake_object(fresh:) object = double('object') + allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(true) allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh) allow(object).to receive(:cached_html_for).with(:field).and_return('field_html') @@ -12,25 +13,38 @@ describe Banzai::Renderer do describe '#render_field' do let(:renderer) { described_class } - subject { renderer.render_field(object, :field) } - context 'with a stale cache' do - let(:object) { fake_object(fresh: false) } + context 'without cache' do + let(:commit) { create(:project, :repository).commit } - it 'caches and returns the result' do - expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + it 'returns cacheless render field' do + expect(renderer).to receive(:cacheless_render_field).with(commit, :title) - is_expected.to eq('field_html') + renderer.render_field(commit, :title) end end - context 'with an up-to-date cache' do - let(:object) { fake_object(fresh: true) } + context 'with cache' do + subject { renderer.render_field(object, :field) } - it 'uses the cache' do - expect(object).to receive(:refresh_markdown_cache!).never + context 'with a stale cache' do + let(:object) { fake_object(fresh: false) } - is_expected.to eq('field_html') + it 'caches and returns the result' do + expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + + is_expected.to eq('field_html') + end + end + + context 'with an up-to-date cache' do + let(:object) { fake_object(fresh: true) } + + it 'uses the cache' do + expect(object).to receive(:refresh_markdown_cache!).never + + is_expected.to eq('field_html') + end end end end diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 0d5fffa38ff..c56b08b18a2 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -214,7 +214,7 @@ end # The background migration relies on a temporary table, hence we're migrating # to a specific version of the database where said table is still present. # -describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170608152748 do +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do let(:migration) { described_class.new } let(:project) { create(:project_empty_repo) } let(:author) { create(:user) } diff --git a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb new file mode 100644 index 00000000000..7bd6a2ead25 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Path do + describe '#valid?' do + context 'when path contains a zero character' do + it 'is not valid' do + expect(described_class.new("something/\255")).not_to be_valid + end + end + + context 'when path is not utf8 string' do + it 'is not valid' do + expect(described_class.new("something/\0")).not_to be_valid + end + end + + context 'when path is valid' do + it 'is valid' do + expect(described_class.new("some/file/path")).to be_valid + end + end + end + + describe '#directory?' do + context 'when path ends with a directory indicator' do + it 'is a directory' do + expect(described_class.new("some/file/dir/")).to be_directory + end + end + + context 'when path does not end with a directory indicator' do + it 'is not a directory' do + expect(described_class.new("some/file")).not_to be_directory + end + end + end + + describe '#name' do + it 'returns a base name' do + expect(described_class.new("some/file").name).to eq 'file' + end + end + + describe '#nodes' do + it 'returns number of path nodes' do + expect(described_class.new("some/dir/file").nodes).to eq 2 + end + end + + describe '#to_s' do + context 'when path is valid' do + it 'returns a string representation of a path' do + expect(described_class.new('some/path').to_s).to eq 'some/path' + end + end + + context 'when path is invalid' do + it 'raises an error' do + expect { described_class.new("invalid/\0").to_s } + .to raise_error ArgumentError + end + end + end +end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index dfbdbee48f7..d39b33a0c05 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -273,6 +273,25 @@ EOT end end + describe '#json_safe_diff' do + let(:project) { create(:project, :repository) } + + it 'fake binary message when it detects binary' do + # Rugged will not detect this as binary, but we can fake it + diff_message = "Binary files files/images/icn-time-tracking.pdf and files/images/icn-time-tracking.pdf differ\n" + binary_diff = described_class.between(project.repository, 'add-pdf-text-binary', 'add-pdf-text-binary^').first + + expect(binary_diff.diff).not_to be_empty + expect(binary_diff.json_safe_diff).to eq(diff_message) + end + + it 'leave non-binary diffs as-is' do + diff = described_class.new(@rugged_diff) + + expect(diff.json_safe_diff).to eq(diff.diff) + end + end + describe '#submodule?' do before do commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 4cfb4b7d357..556a148c3bc 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -390,46 +390,73 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#delete_branch" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.delete_branch("feature") + shared_examples "deleting a branch" do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + it "removes the branch from the repo" do + branch_name = "to-be-deleted-soon" + + repository.create_branch(branch_name) + expect(repository.rugged.branches[branch_name]).not_to be_nil + + repository.delete_branch(branch_name) + expect(repository.rugged.branches[branch_name]).to be_nil + end + + context "when branch does not exist" do + it "raises a DeleteBranchError exception" do + expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError) + end + end end - it "should remove the branch from the repo" do - expect(@repo.rugged.branches["feature"]).to be_nil + context "when Gitaly delete_branch is enabled" do + it_behaves_like "deleting a branch" end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do + it_behaves_like "deleting a branch" end end describe "#create_branch" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - end + shared_examples 'creating a branch' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - it "should create a new branch" do - expect(@repo.create_branch('new_branch', 'master')).not_to be_nil - end + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end - it "should create a new branch with the right name" do - expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch') - end + it "should create a new branch" do + expect(repository.create_branch('new_branch', 'master')).not_to be_nil + end - it "should fail if we create an existing branch" do - @repo.create_branch('duplicated_branch', 'master') - expect {@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + it "should create a new branch with the right name" do + expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') + end + + it "should fail if we create an existing branch" do + repository.create_branch('duplicated_branch', 'master') + expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + end + + it "should fail if we create a branch from a non existing ref" do + expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + end end - it "should fail if we create a branch from a non existing ref" do - expect {@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + context 'when Gitaly create_branch feature is enabled' do + it_behaves_like 'creating a branch' end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'creating a branch' end end @@ -905,7 +932,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'should set the autocrlf option to the provided option' do @repo.autocrlf = :input - File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| + File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, 'config')) do |config_file| expect(config_file.read).to match('autocrlf = input') end end @@ -916,27 +943,37 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#find_branch' do - it 'should return a Branch for master' do - branch = repository.find_branch('master') + shared_examples 'finding a branch' do + it 'should return a Branch for master' do + branch = repository.find_branch('master') - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') - end + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + + it 'should handle non-existent branch' do + branch = repository.find_branch('this-is-garbage') - it 'should handle non-existent branch' do - branch = repository.find_branch('this-is-garbage') + expect(branch).to eq(nil) + end + end - expect(branch).to eq(nil) + context 'when Gitaly find_branch feature is enabled' do + it_behaves_like 'finding a branch' end - it 'should reload Rugged::Repository and return master' do - expect(Rugged::Repository).to receive(:new).twice.and_call_original + context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding a branch' + + it 'should reload Rugged::Repository and return master' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original - repository.find_branch('master') - branch = repository.find_branch('master', force_reload: true) + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end end end @@ -967,7 +1004,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with local and remote branches' do let(:repository) do - Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end before do @@ -1014,7 +1051,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with local and remote branches' do let(:repository) do - Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end before do @@ -1220,7 +1257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#local_branches' do before(:all) do - @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end after(:all) do diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index c07a2d91768..86f7bcb8e38 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -20,6 +20,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(dir.name).to eq('encoding') } it { expect(dir.path).to eq('encoding') } + it { expect(dir.flat_path).to eq('encoding') } it { expect(dir.mode).to eq('40000') } context :subdir do @@ -30,6 +31,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir.name).to eq('html') } it { expect(subdir.path).to eq('files/html') } + it { expect(subdir.flat_path).to eq('files/html') } end context :subdir_file do @@ -40,6 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) } it { expect(subdir_file.name).to eq('popen.rb') } it { expect(subdir_file.path).to eq('files/ruby/popen.rb') } + it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') } end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index e521fcc6dc1..b07462e4978 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -2,45 +2,9 @@ require 'rails_helper' describe Gitlab::Gpg::Commit do describe '#signature' do - let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - - context 'unsigned commit' do - it 'returns nil' do - expect(described_class.new(project, commit_sha).signature).to be_nil - end - end - - context 'known and verified public key' do - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) - end - - before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end - - it 'returns a valid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( - commit_sha: commit_sha, - project: project, - gpg_key: gpg_key, - gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - gpg_key_user_name: GpgHelpers::User1.names.first, - gpg_key_user_email: GpgHelpers::User1.emails.first, - valid_signature: true - ) - end - + shared_examples 'returns the cached signature on second call' do it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) + gpg_commit = described_class.new(commit) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -51,11 +15,140 @@ describe Gitlab::Gpg::Commit do end end - context 'known but unverified public key' do - let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key } + let!(:project) { create :project, :repository, path: 'sample-project' } + let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - before do - allow(Rugged::Commit).to receive(:extract_signature) + context 'unsigned commit' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + + it 'returns nil' do + expect(described_class.new(commit).signature).to be_nil + end + end + + context 'known key' do + context 'user matches the key uid' do + context 'user email matches the email committer' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } + + let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns a valid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'verified' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + + context 'user email does not match the committer email, but is the same user' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } + + let(:user) do + create(:user, email: GpgHelpers::User1.emails.first).tap do |user| + create :email, user: user, email: GpgHelpers::User2.emails.first + end + end + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'same_user_different_email' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + + context 'user email does not match the committer email' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } + + let(:user) { create(:user, email: GpgHelpers::User1.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'other_user' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + end + + context 'user does not match the key uid' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + + let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) .and_return( [ @@ -63,33 +156,27 @@ describe Gitlab::Gpg::Commit do GpgHelpers::User1.signed_commit_base_data ] ) - end - - it 'returns an invalid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( - commit_sha: commit_sha, - project: project, - gpg_key: gpg_key, - gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - gpg_key_user_name: GpgHelpers::User1.names.first, - gpg_key_user_email: GpgHelpers::User1.emails.first, - valid_signature: false - ) - end - - it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) - - expect(gpg_commit).to receive(:using_keychain).and_call_original - gpg_commit.signature + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'unverified_key' + ) + end - # consecutive call - expect(gpg_commit).not_to receive(:using_keychain).and_call_original - gpg_commit.signature + it_behaves_like 'returns the cached signature on second call' end end - context 'unknown public key' do + context 'unknown key' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + before do allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) @@ -102,27 +189,18 @@ describe Gitlab::Gpg::Commit do end it 'returns an invalid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( + expect(described_class.new(commit).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, gpg_key_user_name: nil, gpg_key_user_email: nil, - valid_signature: false + verification_status: 'unknown_key' ) end - it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) - - expect(gpg_commit).to receive(:using_keychain).and_call_original - gpg_commit.signature - - # consecutive call - expect(gpg_commit).not_to receive(:using_keychain).and_call_original - gpg_commit.signature - end + it_behaves_like 'returns the cached signature on second call' end end end diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index 4de4419de27..b9fd4d02156 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -4,8 +4,29 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let!(:project) { create :project, :repository, path: 'sample-project' } + let!(:raw_commit) do + raw_commit = double( + :raw_commit, + signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], + sha: commit_sha, + committer_email: GpgHelpers::User1.emails.first + ) + + allow(raw_commit).to receive :save! + + raw_commit + end + + let!(:commit) do + create :commit, git_commit: raw_commit, project: project + end before do + allow_any_instance_of(Project).to receive(:commit).and_return(commit) + allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) .and_return( @@ -25,7 +46,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' end it 'assigns the gpg key to the signature when the missing gpg key is added' do @@ -39,7 +60,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -54,7 +75,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end end @@ -68,7 +89,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' end it 'updates the signature to being valid when the missing gpg key is added' do @@ -82,7 +103,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -97,7 +118,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' ) end end @@ -115,7 +136,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' end it 'updates the signature to being valid when the user updates the email address' do @@ -123,7 +144,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User1.public_key, user: user - expect(invalid_gpg_signature.reload.valid_signature).to be_falsey + expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key' # InvalidGpgSignatureUpdater is called by the after_update hook user.update_attributes!(email: GpgHelpers::User1.emails.first) @@ -133,7 +154,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -147,7 +168,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unverified_key' ) # InvalidGpgSignatureUpdater is called by the after_update hook @@ -158,7 +179,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unverified_key' ) end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 30ad033b204..11a2aea1915 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -42,6 +42,21 @@ describe Gitlab::Gpg do described_class.user_infos_from_key('bogus') ).to eq [] end + + it 'downcases the email' do + public_key = double(:key) + fingerprints = double(:fingerprints) + uid = double(:uid, name: 'Nannie Bernhard', email: 'NANNIE.BERNHARD@EXAMPLE.COM') + raw_key = double(:raw_key, uids: [uid]) + allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints) + allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key]) + + user_infos = described_class.user_infos_from_key(public_key) + expect(user_infos).to eq([{ + name: 'Nannie Bernhard', + email: 'nannie.bernhard@example.com' + }]) + end end describe '.current_home_dir' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8da02b0cf00..beed4e77e8b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -264,6 +264,7 @@ project: - statistics - container_repositories - uploads +- members_and_requesters award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 5b16fc5d084..d664d371028 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -11,8 +11,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') - allow(@project.repository).to receive(:fetch_ref).and_return(true) - allow(@project.repository.raw).to receive(:rugged_branch_exists?).and_return(false) + allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true) + allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA') allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 065b0ec6658..8e3554375e8 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -117,6 +117,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1) end + it 'has no when YML attributes but only the DB column' do + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) + expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes) + + saved_project_json + end + it 'has pipeline commits' do expect(saved_project_json['pipelines']).not_to be_empty end @@ -251,15 +258,11 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:label_priority, label: group_label, priority: 1) milestone = create(:milestone, project: project) merge_request = create(:merge_request, source_project: project, milestone: milestone) - commit_status = create(:commit_status, project: project) - ci_pipeline = create(:ci_pipeline, - project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch, - statuses: [commit_status]) + ci_build = create(:ci_build, project: project, when: nil) + ci_build.pipeline.update(project: project) + create(:commit_status, project: project, pipeline: ci_build.pipeline) - create(:ci_build, pipeline: ci_pipeline, project: project) create(:milestone, project: project) create(:note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) @@ -267,7 +270,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:note_on_commit, author: user, project: project, - commit_id: ci_pipeline.sha) + commit_id: ci_build.pipeline.sha) create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index b852ac570a3..122b8ee0314 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -65,6 +65,7 @@ Note: - change_position - resolved_at - resolved_by_id +- resolved_by_push - discussion_id - original_discussion_id LabelLink: @@ -398,6 +399,7 @@ Project: - public_builds - last_repository_check_failed - last_repository_check_at +- collapse_outdated_diff_comments - container_registry_enabled - only_allow_merge_if_pipeline_succeeds - has_external_issue_tracker @@ -406,6 +408,7 @@ Project: - only_allow_merge_if_all_discussions_are_resolved - auto_cancel_pending_pipelines - printing_merge_request_link_enabled +- resolve_outdated_diff_discussions - build_allow_git_fetch - last_repository_updated_at - ci_config_path diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb new file mode 100644 index 00000000000..c262fdfcb61 --- /dev/null +++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::IssuablesCountForState do + let(:finder) do + double(:finder, count_by_state: { opened: 2, closed: 1 }) + end + + let(:counter) { described_class.new(finder) } + + describe '#for_state_or_opened' do + it 'returns the number of issuables for the given state' do + expect(counter.for_state_or_opened(:closed)).to eq(1) + end + + it 'returns the number of open issuables when no state is given' do + expect(counter.for_state_or_opened).to eq(2) + end + + it 'returns the number of open issuables when a nil value is given' do + expect(counter.for_state_or_opened(nil)).to eq(2) + end + end + + describe '#[]' do + it 'returns the number of issuables for the given state' do + expect(counter[:closed]).to eq(1) + end + + it 'casts valid states from Strings to Symbols' do + expect(counter['closed']).to eq(1) + end + + it 'returns 0 when using an invalid state name as a String' do + expect(counter['kittens']).to be_zero + end + end +end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 5100a5a609e..6a6e465cea2 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -37,7 +37,8 @@ describe Gitlab::LDAP::User do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', external_email: true, email_provider: 'ldapmain') + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true) expect(ldap_user.changed?).to be_falsey end end @@ -141,12 +142,12 @@ describe Gitlab::LDAP::User do expect(ldap_user.gl_user.email).to eq(info[:email]) end - it "has external_email set to true" do - expect(ldap_user.gl_user.external_email?).to be(true) + it "has user_synced_attributes_metadata email set to true" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy end - it "has email_provider set to provider" do - expect(ldap_user.gl_user.email_provider).to eql 'ldapmain' + it "has synced_attribute_provider set to ldapmain" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain' end end @@ -156,11 +157,11 @@ describe Gitlab::LDAP::User do end it "has a temp email" do - expect(ldap_user.gl_user.temp_oauth_email?).to be(true) + expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy end - it "has external_email set to false" do - expect(ldap_user.gl_user.external_email?).to be(false) + it "has synced attribute email set to false" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end end end @@ -168,7 +169,7 @@ describe Gitlab::LDAP::User do describe 'blocking' do def configure_block(value) allow_any_instance_of(Gitlab::LDAP::Config) - .to receive(:block_auto_created_users).and_return(value) + .to receive(:block_auto_created_users).and_return(value) end context 'signup' do diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 2cf0f7516de..8aaf320cbf5 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -10,7 +10,11 @@ describe Gitlab::OAuth::User do { nickname: '-john+gitlab-ETC%.git@gmail.com', name: 'John', - email: 'john@mail.com' + email: 'john@mail.com', + address: { + locality: 'locality', + country: 'country' + } } end let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } @@ -422,11 +426,12 @@ describe Gitlab::OAuth::User do end end - describe 'updating email' do + describe 'ensure backwards compatibility with with sync email from provider option' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } before do stub_omniauth_config(sync_email_from_provider: 'my-provider') + stub_omniauth_config(sync_profile_from_provider: ['my-provider']) end context "when provider sets an email" do @@ -434,12 +439,12 @@ describe Gitlab::OAuth::User do expect(gl_user.email).to eq(info_hash[:email]) end - it "has external_email set to true" do - expect(gl_user.external_email?).to be(true) + it "has external_attributes set to true" do + expect(gl_user.user_synced_attributes_metadata).not_to be_nil end - it "has email_provider set to provider" do - expect(gl_user.email_provider).to eql 'my-provider' + it "has attributes_provider set to my-provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' end end @@ -452,8 +457,9 @@ describe Gitlab::OAuth::User do expect(gl_user.email).not_to eq(info_hash[:email]) end - it "has external_email set to false" do - expect(gl_user.external_email?).to be(false) + it "has user_synced_attributes_metadata set to nil" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end end end @@ -487,4 +493,172 @@ describe Gitlab::OAuth::User do end end end + + describe 'updating email with sync profile' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_config(sync_profile_from_provider: ['my-provider']) + stub_omniauth_config(sync_profile_attributes: true) + end + + context "when provider sets an email" do + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + end + + it "has email_synced_attribute set to true" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + + it "has my-provider as attributes_provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' + end + end + + context "when provider doesn't set an email" do + before do + info_hash.delete(:email) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false) + end + end + end + + describe 'updating name' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + context "when provider sets a name" do + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + end + end + + context "when provider doesn't set a name" do + before do + info_hash.delete(:name) + end + + it "does not update the user name" do + expect(gl_user.name).not_to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(false) + end + end + end + + describe 'updating location' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + context "when provider sets a location" do + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + end + + context "when provider doesn't set a location" do + before do + info_hash[:address].delete(:country) + info_hash[:address].delete(:locality) + end + + it "does not update the user location" do + expect(gl_user.location).to be_nil + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(false) + end + end + end + + describe 'updating user info' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + context "update all info" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true) + end + + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + + it "sets my-provider as the attributes provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql('my-provider') + end + end + + context "update only requested info" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: %w(name location)) + end + + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true) + end + + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + + it "does not update the user email" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false) + end + end + + context "update default_scope" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + end + + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + end + + context "update no info when profile sync is nil" do + it "does not have sync_attribute" do + expect(gl_user.user_synced_attributes_metadata).to be(nil) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + end + + it "does not update the user name" do + expect(gl_user.name).not_to eq(info_hash[:name]) + end + + it "does not update the user location" do + expect(gl_user.location).not_to eq(info_hash[:address][:country]) + end + end + end end diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 9d7b2136dab..48d56628ed5 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do end end end + + describe '.select_fuzzy_words' do + subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) } + + context 'with a word equal to 3 chars' do + let(:query) { 'foo' } + + it 'returns array cotaining a word' do + expect(select_fuzzy_words).to match_array(['foo']) + end + end + + context 'with a word shorter than 3 chars' do + let(:query) { 'fo' } + + it 'returns empty array' do + expect(select_fuzzy_words).to match_array([]) + end + end + + context 'with two words both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns array containing two words' do + expect(select_fuzzy_words).to match_array(%w[foo baz]) + end + end + + context 'with two words divided by two spaces both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns array containing two words' do + expect(select_fuzzy_words).to match_array(%w[foo baz]) + end + end + + context 'with two words equal to 3 chars and shorter than 3 chars' do + let(:query) { 'foo ba' } + + it 'returns array containing a word' do + expect(select_fuzzy_words).to match_array(['foo']) + end + end + + context 'with a multi-word surrounded by double quote' do + let(:query) { '"really bar"' } + + it 'returns array containing a multi-word' do + expect(select_fuzzy_words).to match_array(['really bar']) + end + end + + context 'with a multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz' } + + it 'returns array containing a multi-word and tow words' do + expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz']) + end + end + + context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do + let(:query) { 'foo"really bar"' } + + it 'returns array containing two words with double quote' do + expect(select_fuzzy_words).to match_array(['foo"really', 'bar"']) + end + end + + context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do + let(:query) { '"really bar"baz' } + + it 'returns array containing two words with double quote' do + expect(select_fuzzy_words).to match_array(['"really', 'bar"baz']) + end + end + + context 'with two multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz "awesome feature"' } + + it 'returns array containing two multi-words and tow words' do + expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature']) + end + end + end + + describe '.to_fuzzy_arel' do + subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) } + + context 'with a word equal to 3 chars' do + let(:query) { 'foo' } + + it 'returns a single ILIKE condition' do + expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/) + end + end + + context 'with a word shorter than 3 chars' do + let(:query) { 'fo' } + + it 'returns nil' do + expect(to_fuzzy_arel).to be_nil + end + end + + context 'with two words both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns a joining LIKE condition using a AND' do + expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/) + end + end + + context 'with a multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz' } + + it 'returns a joining LIKE condition using a AND' do + expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/) + end + end + end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 308b1a128be..fdc3990132a 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -1,11 +1,7 @@ require 'spec_helper' describe Gitlab::UrlSanitizer do - let(:credentials) { { user: 'blah', password: 'password' } } - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: credentials) - end - let(:user) { double(:user, username: 'john.doe') } + using RSpec::Parameterized::TableSyntax describe '.sanitize' do def sanitize_url(url) @@ -16,83 +12,166 @@ describe Gitlab::UrlSanitizer do }) end - it 'mask the credentials from HTTP URLs' do - filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/') + where(:input, :output) do + 'http://user:pass@test.com/root/repoC.git/' | 'http://*****:*****@test.com/root/repoC.git/' + 'https://user:pass@test.com/root/repoA.git/' | 'https://*****:*****@test.com/root/repoA.git/' + 'ssh://user@host.test/path/to/repo.git' | 'ssh://*****@host.test/path/to/repo.git' - expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/") - end + # git protocol does not support authentication but clean any details anyway + 'git://user:pass@host.test/path/to/repo.git' | 'git://*****:*****@host.test/path/to/repo.git' + 'git://host.test/path/to/repo.git' | 'git://host.test/path/to/repo.git' - it 'mask the credentials from HTTPS URLs' do - filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/') + # SCP-style URLs are left unmodified + 'user@server:project.git' | 'user@server:project.git' + 'user:pass@server:project.git' | 'user:pass@server:project.git' - expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/") + # return an empty string for invalid URLs + 'ssh://' | '' end - it 'mask credentials from SSH URLs' do - filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git') - - expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git") + with_them do + it { expect(sanitize_url(input)).to include("repository '#{output}' not found") } end + end - it 'does not modify Git URLs' do - # git protocol does not support authentication - filtered_content = sanitize_url('git://host.test/path/to/repo.git') + describe '.valid?' do + where(:value, :url) do + false | nil + false | '' + false | '123://invalid:url' + true | 'valid@project:url.git' + true | 'ssh://example.com' + true | 'ssh://:@example.com' + true | 'ssh://foo@example.com' + true | 'ssh://foo:bar@example.com' + true | 'ssh://foo:bar@example.com/group/group/project.git' + true | 'git://example.com/group/group/project.git' + true | 'git://foo:bar@example.com/group/group/project.git' + true | 'http://foo:bar@example.com/group/group/project.git' + true | 'https://foo:bar@example.com/group/group/project.git' + end - expect(filtered_content).to include("git://host.test/path/to/repo.git") + with_them do + it { expect(described_class.valid?(url)).to eq(value) } end + end + + describe '#sanitized_url' do + context 'credentials in hash' do + where(username: ['foo', '', nil], password: ['bar', '', nil]) - it 'does not modify scp-like URLs' do - filtered_content = sanitize_url('user@server:project.git') + with_them do + let(:credentials) { { user: username, password: password } } + subject { described_class.new('http://example.com', credentials: credentials).sanitized_url } - expect(filtered_content).to include("user@server:project.git") + it { is_expected.to eq('http://example.com') } + end end - it 'returns an empty string for invalid URLs' do - filtered_content = sanitize_url('ssh://') + context 'credentials in URL' do + where(userinfo: %w[foo:bar@ foo@ :bar@ :@ @] + [nil]) - expect(filtered_content).to include("repository '' not found") - end - end + with_them do + subject { described_class.new("http://#{userinfo}example.com").sanitized_url } - describe '.valid?' do - it 'validates url strings' do - expect(described_class.valid?(nil)).to be(false) - expect(described_class.valid?('valid@project:url.git')).to be(true) - expect(described_class.valid?('123://invalid:url')).to be(false) + it { is_expected.to eq('http://example.com') } + end end end - describe '#sanitized_url' do - it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } - end - describe '#credentials' do - it { expect(url_sanitizer.credentials).to eq(credentials) } + context 'credentials in hash' do + where(:input, :output) do + { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' } + { user: 'foo', password: '' } | { user: 'foo', password: nil } + { user: 'foo', password: nil } | { user: 'foo', password: nil } + { user: '', password: 'bar' } | { user: nil, password: 'bar' } + { user: '', password: '' } | { user: nil, password: nil } + { user: '', password: nil } | { user: nil, password: nil } + { user: nil, password: 'bar' } | { user: nil, password: 'bar' } + { user: nil, password: '' } | { user: nil, password: nil } + { user: nil, password: nil } | { user: nil, password: nil } + end - context 'when user is given to #initialize' do - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: { user: user.username }) + with_them do + subject { described_class.new('user@example.com:path.git', credentials: input).credentials } + + it { is_expected.to eq(output) } end - it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) } + it 'overrides URL-provided credentials' do + sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' }) + + expect(sanitizer.credentials).to eq(user: 'c', password: 'd') + end + end + + context 'credentials in URL' do + where(:url, :credentials) do + 'http://foo:bar@example.com' | { user: 'foo', password: 'bar' } + 'http://:bar@example.com' | { user: nil, password: 'bar' } + 'http://foo:@example.com' | { user: 'foo', password: nil } + 'http://foo@example.com' | { user: 'foo', password: nil } + 'http://:@example.com' | { user: nil, password: nil } + 'http://@example.com' | { user: nil, password: nil } + 'http://example.com' | { user: nil, password: nil } + + # Credentials from SCP-style URLs are not supported at present + 'foo@example.com:path' | { user: nil, password: nil } + 'foo:bar@example.com:path' | { user: nil, password: nil } + + # Other invalid URLs + nil | { user: nil, password: nil } + '' | { user: nil, password: nil } + 'no' | { user: nil, password: nil } + end + + with_them do + subject { described_class.new(url).credentials } + + it { is_expected.to eq(credentials) } + end end end describe '#full_url' do - it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") } + context 'credentials in hash' do + where(:credentials, :userinfo) do + { user: 'foo', password: 'bar' } | 'foo:bar@' + { user: 'foo', password: '' } | 'foo@' + { user: 'foo', password: nil } | 'foo@' + { user: '', password: 'bar' } | ':bar@' + { user: '', password: '' } | nil + { user: '', password: nil } | nil + { user: nil, password: 'bar' } | ':bar@' + { user: nil, password: '' } | nil + { user: nil, password: nil } | nil + end - it 'supports scp-like URLs' do - sanitizer = described_class.new('user@server:project.git') + with_them do + subject { described_class.new('http://example.com', credentials: credentials).full_url } - expect(sanitizer.full_url).to eq('user@server:project.git') + it { is_expected.to eq("http://#{userinfo}example.com") } + end end - context 'when user is given to #initialize' do - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: { user: user.username }) + context 'credentials in URL' do + where(:input, :output) do + nil | '' + '' | :same + 'git@example.com' | :same + 'http://example.com' | :same + 'http://foo@example.com' | :same + 'http://foo:@example.com' | 'http://foo@example.com' + 'http://:bar@example.com' | :same + 'http://foo:bar@example.com' | :same end - it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") } + with_them do + let(:expected) { output == :same ? input : output } + + it { expect(described_class.new(input).full_url).to eq(expected) } + end end end end diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index 4de5da984ba..9da3648400e 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do end end + class DynamicSkipCheck < SystemCheck::BaseCheck + set_name 'dynamic skip check' + set_skip_reason 'this is a skip reason' + + def skip? + self.skip_reason = 'this is a dynamic skip reason' + true + end + + def check? + raise 'should not execute this' + end + end + class MultiCheck < SystemCheck::BaseCheck set_name 'multi check' @@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do expect(subject.checks.size).to eq(1) end + + it 'errors out when passing multiple items' do + expect { subject << [SimpleCheck, OtherCheck] }.to raise_error(ArgumentError) + end end subject { described_class.new('Test') } @@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do subject.run_check(SkipCheck) end - it 'displays #skip_reason' do + it 'displays .skip_reason' do expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout end + it 'displays #skip_reason' do + expect { subject.run_check(DynamicSkipCheck) }.to output(/this is a dynamic skip reason/).to_stdout + end + it 'does not execute #check when #skip? is true' do expect_any_instance_of(SkipCheck).not_to receive(:check?) diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb new file mode 100644 index 00000000000..cfd4021fbac --- /dev/null +++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb') + +describe MigrateIssuesToGhostUser, :migration do + describe '#up' do + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:users) { table(:users) } + + before do + projects.create!(name: 'gitlab') + user = users.create(email: 'test@example.com') + issues.create(title: 'Issue 1', author_id: nil, project_id: 1) + issues.create(title: 'Issue 2', author_id: user.id, project_id: 1) + end + + context 'when ghost user exists' do + let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') } + + it 'does not create a new user' do + expect { schema_migrate_up! }.not_to change { User.count } + end + + it 'migrates issues where author = nil to the ghost user' do + schema_migrate_up! + + expect(issues.first.reload.author_id).to eq(ghost.id) + end + + it 'does not change issues authored by an existing user' do + expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + end + end + + context 'when ghost user does not exist' do + it 'creates a new user' do + expect { schema_migrate_up! }.to change { User.count }.by(1) + end + + it 'migrates issues where author = nil to the ghost user' do + schema_migrate_up! + + expect(issues.first.reload.author_id).to eq(User.ghost.id) + end + + it 'does not change issues authored by an existing user' do + expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index dfbe1a7c192..fb5fb7daaab 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -66,56 +66,76 @@ describe Issuable do end describe ".search" do - let!(:searchable_issue) { create(:issue, title: "Searchable issue") } + let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } - it 'returns notes with a matching title' do + it 'returns issues with a matching title' do expect(issuable_class.search(searchable_issue.title)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching title' do + it 'returns issues with a partially matching title' do expect(issuable_class.search('able')).to eq([searchable_issue]) end - it 'returns notes with a matching title regardless of the casing' do + it 'returns issues with a matching title regardless of the casing' do expect(issuable_class.search(searchable_issue.title.upcase)) .to eq([searchable_issue]) end + + it 'returns issues with a fuzzy matching title' do + expect(issuable_class.search('searchable issue')).to eq([searchable_issue]) + end + + it 'returns all issues with a query shorter than 3 chars' do + expect(issuable_class.search('zz')).to eq(issuable_class.all) + end end describe ".full_search" do let!(:searchable_issue) do - create(:issue, title: "Searchable issue", description: 'kittens') + create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens') end - it 'returns notes with a matching title' do + it 'returns issues with a matching title' do expect(issuable_class.full_search(searchable_issue.title)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching title' do + it 'returns issues with a partially matching title' do expect(issuable_class.full_search('able')).to eq([searchable_issue]) end - it 'returns notes with a matching title regardless of the casing' do + it 'returns issues with a matching title regardless of the casing' do expect(issuable_class.full_search(searchable_issue.title.upcase)) .to eq([searchable_issue]) end - it 'returns notes with a matching description' do + it 'returns issues with a fuzzy matching title' do + expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue]) + end + + it 'returns issues with a matching description' do expect(issuable_class.full_search(searchable_issue.description)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching description' do + it 'returns issues with a partially matching description' do expect(issuable_class.full_search(searchable_issue.description)) .to eq([searchable_issue]) end - it 'returns notes with a matching description regardless of the casing' do + it 'returns issues with a matching description regardless of the casing' do expect(issuable_class.full_search(searchable_issue.description.upcase)) .to eq([searchable_issue]) end + + it 'returns issues with a fuzzy matching description' do + expect(issuable_class.full_search('many kittens')).to eq([searchable_issue]) + end + + it 'returns all issues with a query shorter than 3 chars' do + expect(issuable_class.search('zz')).to eq(issuable_class.all) + end end describe '.to_ability_name' do @@ -460,4 +480,71 @@ describe Issuable do end end end + + describe '#first_contribution?' do + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:other_project) { create(:project) } + let(:owner) { create(:owner) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + + let(:contributor) { create(:user) } + let(:first_time_contributor) { create(:user) } + + before do + group.add_owner(owner) + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) + project.add_guest(contributor) + project.add_guest(first_time_contributor) + end + + let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) } + let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) } + let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) } + + context "for merge requests" do + it "is false for MASTER" do + mr = create(:merge_request, author: master, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is false for OWNER" do + mr = create(:merge_request, author: owner, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is false for REPORTER" do + mr = create(:merge_request, author: reporter, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is true when you don't have any merged MR" do + expect(open_mr).to be_first_contribution + expect(merged_mr).not_to be_first_contribution + end + + it "handles multiple projects separately" do + expect(open_mr).to be_first_contribution + expect(merged_mr_other_project).not_to be_first_contribution + end + end + + context "for issues" do + let(:contributor_issue) { create(:issue, author: contributor, project: project) } + let(:first_time_contributor_issue) { create(:issue, author: first_time_contributor, project: project) } + + it "is false even without merged MR" do + expect(merged_mr).to be + expect(first_time_contributor_issue).not_to be_first_contribution + expect(contributor_issue).not_to be_first_contribution + end + end + end end diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb index d00faa4f8be..91591017587 100644 --- a/spec/models/concerns/resolvable_note_spec.rb +++ b/spec/models/concerns/resolvable_note_spec.rb @@ -189,8 +189,8 @@ describe Note, ResolvableNote do allow(subject).to receive(:resolvable?).and_return(false) end - it "returns nil" do - expect(subject.resolve!(current_user)).to be_nil + it "returns false" do + expect(subject.resolve!(current_user)).to be_falsey end it "doesn't set resolved_at" do @@ -224,8 +224,8 @@ describe Note, ResolvableNote do subject.resolve!(user) end - it "returns nil" do - expect(subject.resolve!(current_user)).to be_nil + it "returns false" do + expect(subject.resolve!(current_user)).to be_falsey end it "doesn't change resolved_at" do @@ -279,8 +279,8 @@ describe Note, ResolvableNote do allow(subject).to receive(:resolvable?).and_return(false) end - it "returns nil" do - expect(subject.unresolve!).to be_nil + it "returns false" do + expect(subject.unresolve!).to be_falsey end end @@ -320,8 +320,8 @@ describe Note, ResolvableNote do end context "when not resolved" do - it "returns nil" do - expect(subject.unresolve!).to be_nil + it "returns false" do + expect(subject.unresolve!).to be_falsey end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index e48f20bf53b..9c99c3e5c08 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -99,14 +99,14 @@ describe GpgKey do end describe '#verified?' do - it 'returns true one of the email addresses in the key belongs to the user' do + it 'returns true if one of the email addresses in the key belongs to the user' do user = create :user, email: 'bette.cartwright@example.com' gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user expect(gpg_key.verified?).to be_truthy end - it 'returns false if one of the email addresses in the key does not belong to the user' do + it 'returns false if none of the email addresses in the key does not belong to the user' do user = create :user, email: 'someone.else@example.com' gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user @@ -114,6 +114,32 @@ describe GpgKey do end end + describe 'verified_and_belongs_to_email?' do + it 'returns false if none of the email addresses in the key does not belong to the user' do + user = create :user, email: 'someone.else@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_falsey + expect(gpg_key.verified_and_belongs_to_email?('someone.else@example.com')).to be_falsey + end + + it 'returns false if one of the email addresses in the key belongs to the user and does not match the provided email' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_truthy + expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.net')).to be_falsey + end + + it 'returns true if one of the email addresses in the key belongs to the user and matches the provided email' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_truthy + expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.com')).to be_truthy + end + end + describe 'notification', :mailer do let(:user) { create(:user) } @@ -129,15 +155,15 @@ describe GpgKey do describe '#revoke' do it 'invalidates all associated gpg signatures and destroys the key' do gpg_key = create :gpg_key - gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: gpg_key + gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key unrelated_gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key - unrelated_gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: unrelated_gpg_key + unrelated_gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: unrelated_gpg_key gpg_key.revoke expect(gpg_signature.reload).to have_attributes( - valid_signature: false, + verification_status: 'unknown_key', gpg_key: nil ) @@ -145,7 +171,7 @@ describe GpgKey do # unrelated signature is left untouched expect(unrelated_gpg_signature.reload).to have_attributes( - valid_signature: true, + verification_status: 'verified', gpg_key: unrelated_gpg_key ) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f9cd12c0ff3..f36d6eeb327 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -9,6 +9,7 @@ describe Group do it { is_expected.to have_many(:users).through(:group_members) } it { is_expected.to have_many(:owners).through(:group_members) } it { is_expected.to have_many(:requesters).dependent(:destroy) } + it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } @@ -25,22 +26,8 @@ describe Group do group.add_developer(developer) end - describe '#members' do - it 'includes members and exclude requesters' do - member_user_ids = group.members.pluck(:user_id) - - expect(member_user_ids).to include(developer.id) - expect(member_user_ids).not_to include(requester.id) - end - end - - describe '#requesters' do - it 'does not include requesters' do - requester_user_ids = group.requesters.pluck(:user_id) - - expect(requester_user_ids).to include(requester.id) - expect(requester_user_ids).not_to include(developer.id) - end + it_behaves_like 'members and requesters associations' do + let(:namespace) { group } end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 87513e18b25..a07ce05a865 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -409,6 +409,15 @@ describe Member do expect(members).to be_a Array expect(members).to be_empty end + + it 'supports differents formats' do + list = ['joe@local.test', admin, user1.id, user2.id.to_s] + + members = described_class.add_users(source, list, :master) + + expect(members.size).to eq(4) + expect(members.first).to be_invite + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f5d079c27c4..d80d5657c42 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1262,7 +1262,6 @@ describe MergeRequest do describe "#reload_diff" do let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion } - let(:commit) { subject.project.commit(sample_commit.id) } it "does not change existing merge request diff" do @@ -1280,9 +1279,19 @@ describe MergeRequest do subject.reload_diff end - it "updates diff discussion positions" do - old_diff_refs = subject.diff_refs + it "calls update_diff_discussion_positions" do + expect(subject).to receive(:update_diff_discussion_positions) + + subject.reload_diff + end + end + describe '#update_diff_discussion_positions' do + let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion } + let(:commit) { subject.project.commit(sample_commit.id) } + let(:old_diff_refs) { subject.diff_refs } + + before do # Update merge_request_diff so that #diff_refs will return commit.diff_refs allow(subject).to receive(:create_merge_request_diff) do subject.merge_request_diffs.create( @@ -1293,7 +1302,9 @@ describe MergeRequest do subject.merge_request_diff(true) end + end + it "updates diff discussion positions" do expect(Discussions::UpdateDiffPositionService).to receive(:new).with( subject.project, subject.author, @@ -1305,7 +1316,26 @@ describe MergeRequest do expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original expect_any_instance_of(DiffNote).to receive(:save).once - subject.reload_diff(subject.author) + subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + current_user: subject.author) + end + + context 'when resolve_outdated_diff_discussions is set' do + before do + discussion + + subject.project.update!(resolve_outdated_diff_discussions: true) + end + + it 'calls MergeRequests::ResolvedDiscussionNotificationService' do + expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService) + .to receive(:execute).with(subject) + + subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + current_user: subject.author) + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index be1ae295f75..1f7c6a82b91 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -74,6 +74,7 @@ describe Project do it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:pipeline_schedules) } + it { is_expected.to have_many(:members_and_requesters) } context 'after initialized' do it "has a project_feature" do @@ -90,22 +91,8 @@ describe Project do project.team << [developer, :developer] end - describe '#members' do - it 'includes members and exclude requesters' do - member_user_ids = project.members.pluck(:user_id) - - expect(member_user_ids).to include(developer.id) - expect(member_user_ids).not_to include(requester.id) - end - end - - describe '#requesters' do - it 'does not include requesters' do - requester_user_ids = project.requesters.pluck(:user_id) - - expect(requester_user_ids).to include(requester.id) - expect(requester_user_ids).not_to include(developer.id) - end + it_behaves_like 'members and requesters associations' do + let(:namespace) { project } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b70ab5581ac..abf732e60bf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2102,4 +2102,84 @@ describe User do end end end + + describe '#verified_email?' do + it 'returns true when the email is the primary email' do + user = build :user, email: 'email@example.com' + + expect(user.verified_email?('email@example.com')).to be true + end + + it 'returns false when the email is not the primary email' do + user = build :user, email: 'email@example.com' + + expect(user.verified_email?('other_email@example.com')).to be false + end + end + + describe '#sync_attribute?' do + let(:user) { described_class.new } + + context 'oauth user' do + it 'returns true if name can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(name location)) + expect(user.sync_attribute?(:name)).to be_truthy + end + + it 'returns true if email can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(name email)) + expect(user.sync_attribute?(:email)).to be_truthy + end + + it 'returns true if location can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:email)).to be_truthy + end + + it 'returns false if name can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns false if email can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns false if location can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns true for all syncable attributes if all syncable attributes can be synced' do + stub_omniauth_setting(sync_profile_attributes: true) + expect(user.sync_attribute?(:name)).to be_truthy + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_truthy + end + + it 'returns false for all syncable attributes but email if no syncable attributes are declared' do + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_falsey + end + end + + context 'ldap user' do + it 'returns true for email if ldap user' do + allow(user).to receive(:ldap_user?).and_return(true) + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_falsey + end + + it 'returns true for email and location if ldap user and location declared as syncable' do + allow(user).to receive(:ldap_user?).and_return(true) + stub_omniauth_setting(sync_profile_attributes: %w(location)) + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_truthy + end + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b1e011de604..cc794fad3a7 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -75,6 +75,22 @@ describe API::Branches do let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}" } shared_examples_for 'repository branch' do + context 'HEAD request' do + it 'returns 204 No Content' do + head api(route, user) + + expect(response).to have_gitlab_http_status(204) + expect(response.body).to be_empty + end + + it 'returns 404 Not Found' do + head api("/projects/#{project_id}/repository/branches/unknown", user) + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_empty + end + end + it 'returns the repository branch' do get api(route, current_user) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index edbfaf510c5..f663719d28c 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -673,6 +673,12 @@ describe API::Commits do it_behaves_like 'ref diff' end end + + context 'when binary diff are treated as text' do + let(:commit_id) { TestEnv::BRANCH_SHA['add-pdf-text-binary'] } + + it_behaves_like 'ref diff' + end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index a6c804fb2b3..1274e66bb4c 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,13 +5,26 @@ describe API::Internal do let(:key) { create(:key, user: user) } let(:project) { create(:project, :repository) } let(:secret_token) { Gitlab::Shell.secret_token } + let(:gl_repository) { "project-#{project.id}" } + let(:reference_counter) { double('ReferenceCounter') } describe "GET /internal/check" do it do + expect_any_instance_of(Redis).to receive(:ping).and_return('PONG') + get api("/internal/check"), secret_token: secret_token expect(response).to have_http_status(200) expect(json_response['api_version']).to eq(API::API.version) + expect(json_response['redis']).to be(true) + end + + it 'returns false for field `redis` when redis is unavailable' do + expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT) + + get api("/internal/check"), secret_token: secret_token + + expect(json_response['redis']).to be(false) end end @@ -661,9 +674,7 @@ describe API::Internal do # end describe 'POST /internal/post_receive' do - let(:gl_repository) { "project-#{project.id}" } let(:identifier) { 'key-123' } - let(:reference_counter) { double('ReferenceCounter') } let(:valid_params) do { @@ -749,6 +760,22 @@ describe API::Internal do end end + describe 'POST /internal/pre_receive' do + let(:valid_params) do + { gl_repository: gl_repository, secret_token: secret_token } + end + + it 'decreases the reference counter and returns the result' do + expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository) + .and_return(reference_counter) + expect(reference_counter).to receive(:increase).and_return(true) + + post api("/internal/pre_receive"), valid_params + + expect(json_response['reference_counter_increased']).to be(true) + end + end + def project_with_repo_path(path) double().tap do |fake_project| allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index f56baf9663d..2d7cc1a1798 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe API::Jobs do - let!(:project) do + set(:project) do create(:project, :repository, public_builds: false) end - let!(:pipeline) do + set(:pipeline) do create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) @@ -188,6 +188,84 @@ describe API::Jobs do end end + describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + let(:artifact) do + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + context 'when project is public' do + it 'allows to access artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_http_status(200) + end + end + + context 'when project is public with builds access disabled' do + it 'rejects access to artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, false) + + get_artifact_file(artifact) + + expect(response).to have_http_status(403) + end + end + + context 'when project is private' do + it 'rejects access and hides existence of artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PRIVATE) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_http_status(404) + end + end + end + + context 'when user is authorized' do + it 'returns a specific artifact file for a valid path' do + expect(Gitlab::Workhorse) + .to receive(:send_artifacts_entry) + .and_call_original + + get_artifact_file(artifact) + + expect(response).to have_http_status(200) + expect(response.headers) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + end + + context 'when job does not have artifacts' do + it 'does not return job artifact file' do + get_artifact_file('some/artifact') + + expect(response).to have_http_status(404) + end + end + + def get_artifact_file(artifact_path) + get api("/projects/#{project.id}/jobs/#{job.id}/" \ + "artifacts/#{artifact_path}", api_user) + end + end + describe 'GET /projects/:id/jobs/:job_id/artifacts' do before do get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) @@ -209,11 +287,12 @@ describe API::Jobs do end end - context 'unauthorized user' do + context 'when anonymous user is accessing private artifacts' do let(:api_user) { nil } - it 'does not return specific job artifacts' do - expect(response).to have_http_status(401) + it 'hides artifacts and rejects request' do + expect(project).to be_private + expect(response).to have_http_status(404) end end end @@ -242,8 +321,9 @@ describe API::Jobs do get_for_ref end - it 'gives 401' do - expect(response).to have_http_status(401) + it 'does not find a resource in a private project' do + expect(project).to be_private + expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4490e50702b..f771e4fa4ff 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -414,6 +414,7 @@ describe API::Projects do jobs_enabled: false, merge_requests_enabled: false, wiki_enabled: false, + resolve_outdated_diff_discussions: false, only_allow_merge_if_pipeline_succeeds: false, request_access_enabled: true, only_allow_merge_if_all_discussions_are_resolved: false, @@ -477,20 +478,40 @@ describe API::Projects do expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif") end + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: false) + + post api('/projects', user), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_falsey + end + + it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do + project = attributes_for(:project, resolve_outdated_diff_discussions: true) + + post api('/projects', user), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_truthy + end + it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) post api('/projects', user), project @@ -506,7 +527,7 @@ describe API::Projects do end it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) post api('/projects', user), project @@ -514,7 +535,7 @@ describe API::Projects do end it 'ignores import_url when it is nil' do - project = attributes_for(:project, { import_url: nil }) + project = attributes_for(:project, import_url: nil) post api('/projects', user), project @@ -642,20 +663,36 @@ describe API::Projects do expect(json_response['visibility']).to eq('private') end + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: false) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_falsey + end + + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: true) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_truthy + end + it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) post api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) + it 'sets a project as allowing merge only if pipeline succeeds' do + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) post api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) post api("/projects/user/#{user.id}", admin), project @@ -663,7 +700,7 @@ describe API::Projects do end it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) post api("/projects/user/#{user.id}", admin), project @@ -732,6 +769,7 @@ describe API::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present expect(json_response['last_activity_at']).to be_present diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5fef4437997..37cb95a16e3 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -4,6 +4,7 @@ describe API::Users do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } + let(:gpg_key) { create(:gpg_key, user: user) } let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } @@ -753,6 +754,164 @@ describe API::Users do end end + describe 'POST /users/:id/keys' do + before do + admin + end + + it 'does not create invalid GPG key' do + post api("/users/#{user.id}/gpg_keys", admin) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('key is missing') + end + + it 'creates GPG key' do + key_attrs = attributes_for :gpg_key + expect do + post api("/users/#{user.id}/gpg_keys", admin), key_attrs + + expect(response).to have_http_status(201) + end.to change { user.gpg_keys.count }.by(1) + end + + it 'returns 400 for invalid ID' do + post api('/users/999999/gpg_keys', admin) + + expect(response).to have_http_status(400) + end + end + + describe 'GET /user/:id/gpg_keys' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get api("/users/#{user.id}/gpg_keys") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get api('/users/999999/gpg_keys', admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + delete api("/users/#{user.id}/gpg_keys/42", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns array of GPG keys' do + user.gpg_keys << gpg_key + user.save + + get api("/users/#{user.id}/gpg_keys", admin) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['key']).to eq(gpg_key.key) + end + end + end + + describe 'DELETE /user/:id/gpg_keys/:key_id' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + delete api("/users/#{user.id}/keys/42") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'deletes existing key' do + user.gpg_keys << gpg_key + user.save + + expect do + delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(204) + end.to change { user.gpg_keys.count }.by(-1) + end + + it 'returns 404 error if user not found' do + user.keys << key + user.save + + delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + delete api("/users/#{user.id}/gpg_keys/42", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + end + end + + describe 'POST /user/:id/gpg_keys/:key_id/revoke' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/users/#{user.id}/gpg_keys/42/revoke") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'revokes existing key' do + user.gpg_keys << gpg_key + user.save + + expect do + post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin) + + expect(response).to have_http_status(:accepted) + end.to change { user.gpg_keys.count }.by(-1) + end + + it 'returns 404 error if user not found' do + user.gpg_keys << gpg_key + user.save + + post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + post api("/users/#{user.id}/gpg_keys/42/revoke", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + end + end + describe "POST /users/:id/emails" do before do admin @@ -1153,6 +1312,173 @@ describe API::Users do end end + describe 'GET /user/gpg_keys' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/user/gpg_keys') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns array of GPG keys' do + user.gpg_keys << gpg_key + user.save + + get api('/user/gpg_keys', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['key']).to eq(gpg_key.key) + end + + context 'scopes' do + let(:path) { '/user/gpg_keys' } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end + end + end + + describe 'GET /user/gpg_keys/:key_id' do + it 'returns a single key' do + user.gpg_keys << gpg_key + user.save + + get api("/user/gpg_keys/#{gpg_key.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['key']).to eq(gpg_key.key) + end + + it 'returns 404 Not Found within invalid ID' do + get api('/user/gpg_keys/42', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it "returns 404 error if admin accesses user's GPG key" do + user.gpg_keys << gpg_key + user.save + + get api("/user/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 404 for invalid ID' do + get api('/users/gpg_keys/ASDF', admin) + + expect(response).to have_http_status(404) + end + + context 'scopes' do + let(:path) { "/user/gpg_keys/#{gpg_key.id}" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end + end + + describe 'POST /user/gpg_keys' do + it 'creates a GPG key' do + key_attrs = attributes_for :gpg_key + expect do + post api('/user/gpg_keys', user), key_attrs + + expect(response).to have_http_status(201) + end.to change { user.gpg_keys.count }.by(1) + end + + it 'returns a 401 error if unauthorized' do + post api('/user/gpg_keys'), key: 'some key' + + expect(response).to have_http_status(401) + end + + it 'does not create GPG key without key' do + post api('/user/gpg_keys', user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('key is missing') + end + end + + describe 'POST /user/gpg_keys/:key_id/revoke' do + it 'revokes existing GPG key' do + user.gpg_keys << gpg_key + user.save + + expect do + post api("/user/gpg_keys/#{gpg_key.id}/revoke", user) + + expect(response).to have_http_status(:accepted) + end.to change { user.gpg_keys.count}.by(-1) + end + + it 'returns 404 if key ID not found' do + post api('/user/gpg_keys/42/revoke', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 401 error if unauthorized' do + user.gpg_keys << gpg_key + user.save + + post api("/user/gpg_keys/#{gpg_key.id}/revoke") + + expect(response).to have_http_status(401) + end + + it 'returns a 404 for invalid ID' do + post api('/users/gpg_keys/ASDF/revoke', admin) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE /user/gpg_keys/:key_id' do + it 'deletes existing GPG key' do + user.gpg_keys << gpg_key + user.save + + expect do + delete api("/user/gpg_keys/#{gpg_key.id}", user) + + expect(response).to have_http_status(204) + end.to change { user.gpg_keys.count}.by(-1) + end + + it 'returns 404 if key ID not found' do + delete api('/user/gpg_keys/42', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 401 error if unauthorized' do + user.gpg_keys << gpg_key + user.save + + delete api("/user/gpg_keys/#{gpg_key.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 404 for invalid ID' do + delete api('/users/gpg_keys/ASDF', admin) + + expect(response).to have_http_status(404) + end + end + describe "GET /user/emails" do context "when unauthenticated" do it "returns authentication error" do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index a514166274a..cae2c3118da 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -687,6 +687,7 @@ describe API::V3::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['builds_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present expect(json_response['last_activity_at']).to be_present diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index f5ed9ff608f..bbc3a8c79f5 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -52,6 +52,17 @@ describe Ci::RetryBuildService do expect(new_build.send(attribute)).to eq build.send(attribute) end end + + context 'when job has nullified protected' do + before do + build.update_attribute(:protected, nil) + end + + it "clones protected build attribute" do + expect(new_build.protected).to be_nil + expect(new_build.protected).to eq build.protected + end + end end describe 'reject acessors' do diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index c239494298b..82b156f5ebe 100644 --- a/spec/services/discussions/update_diff_position_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -150,21 +150,7 @@ describe Discussions::UpdateDiffPositionService do ) end - context "when the diff line is the same" do - let(:line) { 16 } - - it "updates the position" do - subject.execute(discussion) - - expect(discussion.original_position).to eq(old_position) - expect(discussion.position).not_to eq(old_position) - expect(discussion.position.new_line).to eq(22) - end - end - - context "when the diff line has changed" do - let(:line) { 9 } - + shared_examples 'outdated diff note' do it "doesn't update the position" do subject.execute(discussion) @@ -189,5 +175,51 @@ describe Discussions::UpdateDiffPositionService do subject.execute(discussion) end end + + context "when the diff line is the same" do + let(:line) { 16 } + + it "updates the position" do + subject.execute(discussion) + + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).not_to eq(old_position) + expect(discussion.position.new_line).to eq(22) + end + + context 'when the resolve_outdated_diff_discussions setting is set' do + before do + project.update!(resolve_outdated_diff_discussions: true) + end + + it 'does not resolve the discussion' do + subject.execute(discussion) + + expect(discussion).not_to be_resolved + expect(discussion).not_to be_resolved_by_push + end + end + end + + context "when the diff line has changed" do + let(:line) { 9 } + + include_examples 'outdated diff note' + + context 'when the resolve_outdated_diff_discussions setting is set' do + before do + project.update!(resolve_outdated_diff_discussions: true) + end + + it 'sets resolves the discussion and sets resolved_by_push' do + subject.execute(discussion) + + expect(discussion).to be_resolved + expect(discussion).to be_resolved_by_push + end + + include_examples 'outdated diff note' + end + end end end diff --git a/spec/support/group_members_shared_example.rb b/spec/support/group_members_shared_example.rb new file mode 100644 index 00000000000..547c83c7955 --- /dev/null +++ b/spec/support/group_members_shared_example.rb @@ -0,0 +1,27 @@ +RSpec.shared_examples 'members and requesters associations' do + describe '#members_and_requesters' do + it 'includes members and requesters' do + member_and_requester_user_ids = namespace.members_and_requesters.pluck(:user_id) + + expect(member_and_requester_user_ids).to include(requester.id, developer.id) + end + end + + describe '#members' do + it 'includes members and exclude requesters' do + member_user_ids = namespace.members.pluck(:user_id) + + expect(member_user_ids).to include(developer.id) + expect(member_user_ids).not_to include(requester.id) + end + end + + describe '#requesters' do + it 'does not include requesters' do + requester_user_ids = namespace.requesters.pluck(:user_id) + + expect(requester_user_ids).to include(requester.id) + expect(requester_user_ids).not_to include(developer.id) + end + end +end diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 8731847592b..11ef1fc477f 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -41,7 +41,7 @@ module SeedHelper end def create_mutable_seeds - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), chdir: SEED_STORAGE_PATH, out: '/dev/null', err: '/dev/null') diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1e39f80699c..71b9deeabc3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,7 +5,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { - 'signed-commits' => '5d4a1cb', + 'signed-commits' => '2d1096e', 'not-merged-branch' => 'b83d6e3', 'branch-merged' => '498214d', 'empty-branch' => '7efb185', @@ -176,6 +176,24 @@ module TestEnv spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + wait_gitaly + end + + def wait_gitaly + sleep_time = 10 + sleep_interval = 0.1 + socket = Gitlab::GitalyClient.address('default').sub('unix:', '') + + Integer(sleep_time / sleep_interval).times do + begin + Socket.unix(socket) + return + rescue + sleep sleep_interval + end + end + + raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds" end def stop_gitaly diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index faea2505e40..b17bc6692f3 100644 --- a/spec/views/layouts/nav/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' -describe 'layouts/nav/_project' do +describe 'layouts/nav/sidebar/_project' do describe 'container registry tab' do before do + project = create(:project, :repository) stub_container_registry_config(enabled: true) - assign(:project, create(:project, :repository)) + assign(:project, project) + assign(:repository, project.repository) allow(view).to receive(:current_ref).and_return('master') allow(view).to receive(:can?).and_return(true) diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb index 54978baca88..aa6c347d738 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -7,9 +7,14 @@ describe CreateGpgSignatureWorker do let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } it 'calls Gitlab::Gpg::Commit#signature' do - expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original + commit = instance_double(Commit) + gpg_commit = instance_double(Gitlab::Gpg::Commit) - expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature) + allow(Project).to receive(:find_by).with(id: project.id).and_return(project) + allow(project).to receive(:commit).with(commit_sha).and_return(commit) + + expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit) + expect(gpg_commit).to receive(:signature) described_class.new.perform(commit_sha, project.id) end diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index ff23445e2b0..345e61ae3f2 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -31,7 +31,7 @@ cmake-build-debug/ ## Plugin-specific files: # IntelliJ -/out/ +out/ # mpeltonen/sbt-idea plugin .idea_modules/ diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore index 450f32ec40c..eee88b2f0f7 100644 --- a/vendor/gitignore/Haskell.gitignore +++ b/vendor/gitignore/Haskell.gitignore @@ -18,3 +18,4 @@ cabal.sandbox.config .stack-work/ cabal.project.local .HTF/ +.ghc.environment.* diff --git a/vendor/gitignore/Prestashop.gitignore b/vendor/gitignore/Prestashop.gitignore index 7c6ae1e31cc..81f45e19eba 100644 --- a/vendor/gitignore/Prestashop.gitignore +++ b/vendor/gitignore/Prestashop.gitignore @@ -7,8 +7,10 @@ config/settings.*.php # The following files are generated by PrestaShop. admin-dev/autoupgrade/ -/cache/ +/cache/* !/cache/index.php +!/cache/*/ +/cache/*/* !/cache/cachefs/index.php !/cache/purifier/index.php !/cache/push/index.php diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore index 75272b23472..943995e1172 100644 --- a/vendor/gitignore/Smalltalk.gitignore +++ b/vendor/gitignore/Smalltalk.gitignore @@ -13,6 +13,10 @@ SqueakDebug.log # Monticello package cache /package-cache +# playground cache +/play-cache +/play-stash + # Metacello-github cache /github-cache github-*.zip diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index 6c224e024e9..85fd714a965 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -39,3 +39,6 @@ # Backup entities generated with doctrine:generate:entities command **/Entity/*~ + +# Embedded web-server pid file +/.web-server-pid diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 22fd88a55a3..89c66054885 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -151,7 +151,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml index e23b6e212f0..8a214352d2a 100644 --- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -1,14 +1,19 @@ image: golang:latest +variables: + # Please edit to your GitLab project + REPO_NAME: gitlab.com/namespace/project + # The problem is that to be able to use go get, one needs to put # the repository in the $GOPATH. So for example if your gitlab domain -# is mydomainperso.com, and that your repository is repos/projectname, and +# is gitlab.com, and that your repository is namespace/project, and # the default GOPATH being /go, then you'd need to have your -# repository in /go/src/mydomainperso.com/repos/projectname +# repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - - ln -s /builds /go/src/mydomainperso.com - - cd /go/src/mydomainperso.com/repos/projectname + - mkdir -p $GOPATH/src/$REPO_NAME + - ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME + - cd $GOPATH/src/$REPO_NAME stages: - test @@ -17,21 +22,14 @@ stages: format: stage: test script: - # Add here all the dependencies, or use glide/govendor to get - # them automatically. - # - curl https://glide.sh/get | sh - - go get github.com/alecthomas/kingpin - - go tool vet -composites=false -shadow=true *.go - - go test -race $(go list ./... | grep -v /vendor/) + - go fmt $(go list ./... | grep -v /vendor/) + - go vet $(go list ./... | grep -v /vendor/) + - go test -race $(go list ./... | grep -v /vendor/) compile: stage: build script: - # Add here all the dependencies, or use glide/govendor/... - # to get them automatically. - - go get github.com/alecthomas/kingpin - # Better put this in a Makefile - - go build -race -ldflags "-extldflags '-static'" -o mybinary + - go build -race -ldflags "-extldflags '-static'" -o mybinary artifacts: - paths: - - mybinary + paths: + - mybinary diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml index a65e48a3389..48d98dddfad 100644 --- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml @@ -1,41 +1,36 @@ -# This template uses the java:8 docker image because there isn't any -# official Gradle image at this moment -# # This is the Gradle build system for JVM applications # https://gradle.org/ # https://github.com/gradle/gradle -image: java:8 +image: gradle:alpine # Disable the Gradle daemon for Continuous Integration servers as correctness # is usually a priority over speed in CI environments. Using a fresh # runtime for each build is more reliable since the runtime is completely # isolated from any previous builds. variables: - GRADLE_OPTS: "-Dorg.gradle.daemon=false" + GRADLE_OPTS: "-Dorg.gradle.daemon=false" -# Make the gradle wrapper executable. This essentially downloads a copy of -# Gradle to build the project with. -# https://docs.gradle.org/current/userguide/gradle_wrapper.html -# It is expected that any modern gradle project has a wrapper before_script: - - chmod +x gradlew + - export GRADLE_USER_HOME=`pwd`/.gradle -# We redirect the gradle user home using -g so that it caches the -# wrapper and dependencies. -# https://docs.gradle.org/current/userguide/gradle_command_line.html -# -# Unfortunately it also caches the build output so -# cleaning removes reminants of any cached builds. -# The assemble task actually builds the project. -# If it fails here, the tests can't run. build: stage: build - script: - - ./gradlew -g /cache/.gradle clean assemble - allow_failure: false + script: gradle --build-cache assemble + cache: + key: "$CI_COMMIT_REF_NAME" + policy: push + paths: + - build + - .gradle + -# Use the generated build output to run the tests. test: stage: test - script: - - ./gradlew -g /cache/.gradle check + script: gradle check + cache: + key: "$CI_COMMIT_REF_NAME" + policy: pull + paths: + - build + - .gradle + diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml index 434de4f055a..0ad662cf704 100644 --- a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml @@ -34,6 +34,10 @@ before_script: # Install php extensions - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache + # Install & enable Xdebug for code coverage reports + - pecl install xdebug + - docker-php-ext-enable xdebug + # Install Composer and project dependencies. - curl -sS https://getcomposer.org/installer | php - php composer.phar install diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml index bb8caa49d6b..33f44ee9222 100644 --- a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml @@ -11,6 +11,9 @@ before_script: - apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev # Install PHP extensions - docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache +# Install & enable Xdebug for code coverage reports +- pecl install xdebug +- docker-php-ext-enable xdebug # Install and run Composer - curl -sS https://getcomposer.org/installer | php - php composer.phar install diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml index 4e181e85451..ff7bdd32239 100644 --- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml @@ -1,6 +1,6 @@ # Official language image. Look for the different tagged releases at: # https://hub.docker.com/r/library/ruby/tags/ -image: "ruby:2.3" +image: "ruby:2.4" # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. @@ -40,9 +40,9 @@ rails: variables: DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" script: - - bundle exec rake db:migrate - - bundle exec rake db:seed - - bundle exec rake test + - rails db:migrate + - rails db:seed + - rails test # This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk # are supported too: https://github.com/travis-ci/dpl |