diff options
95 files changed, 1398 insertions, 403 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e40e4fc339c..328185caaeb 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.66.0 +0.67.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e030a0157c9..c68d476cc8e 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.10.3 +5.11.0 @@ -70,6 +70,10 @@ gem 'net-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order gem 'gollum-lib', '~> 4.2', require: false + +# Before updating this gem, check if +# https://github.com/gollum/rugged_adapter/pull/28 has been merged. +# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb gem 'gollum-rugged_adapter', '~> 0.4.4', require: false # Language detection @@ -402,7 +406,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.69.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d10da1bd1c3..40c4f73b8a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,7 +284,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.64.0) + gitaly-proto (0.69.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -1053,7 +1053,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.64.0) + gitaly-proto (~> 0.69.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 34e905222b4..8d021de7998 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -7,6 +7,7 @@ import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; +import '../preview_markdown'; installGlEmojiElement(); initCopyAsGFM(); diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 84b76a6f1b1..d8cf532fe78 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -187,7 +187,7 @@ export default { <li class="board-list-count text-center" v-if="showCount" - data-id="-1"> + data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 42f61d33f6e..81d16cdbbe7 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -56,7 +56,6 @@ import GfmAutoComplete from './gfm_auto_complete'; import ShortcutsBlob from './shortcuts_blob'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import Star from './star'; -import Todos from './todos'; import TreeView from './tree'; import UsagePing from './usage_ping'; import UsernameValidator from './username_validator'; @@ -111,6 +110,7 @@ import Activities from './activities'; } const fail = () => Flash('Error loading dynamic module'); + const callDefault = m => m.default(); path = page.split(':'); shortcut_handler = null; @@ -190,15 +190,25 @@ import Activities from './activities'; initIssuableSidebar(); break; case 'dashboard:milestones:index': - projectSelect(); + import('./pages/dashboard/milestones/index') + .then(callDefault) + .catch(fail); break; case 'projects:milestones:show': case 'groups:milestones:show': - case 'dashboard:milestones:show': new Milestone(); new Sidebar(); break; + case 'dashboard:milestones:show': + import('./pages/dashboard/milestones/show') + .then(callDefault) + .catch(fail); + break; case 'dashboard:issues': + import('./pages/dashboard/issues') + .then(callDefault) + .catch(fail); + break; case 'dashboard:merge_requests': projectSelect(); initLegacyFilters(); @@ -212,10 +222,14 @@ import Activities from './activities'; projectSelect(); break; case 'dashboard:todos:index': - new Todos(); + import('./pages/dashboard/todos/index').then(callDefault).catch(fail); break; case 'dashboard:projects:index': case 'dashboard:projects:starred': + import('./pages/dashboard/projects') + .then(callDefault) + .catch(fail); + break; case 'explore:projects:index': case 'explore:projects:trending': case 'explore:projects:starred': @@ -542,7 +556,7 @@ import Activities from './activities'; new CILintEditor(); break; case 'users:show': - import('./pages/users/show').then(m => m.default()).catch(fail); + import('./pages/users/show').then(callDefault).catch(fail); break; case 'admin:conversational_development_index:show': new UserCallout(); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 59bfa482bb0..ce6f91439b4 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -46,7 +46,6 @@ import LazyLoader from './lazy_loader'; import './line_highlighter'; import initLogoAnimation from './logo'; import './milestone_select'; -import './preview_markdown'; import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js new file mode 100644 index 00000000000..0f2f1bd4a25 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -0,0 +1,3 @@ +import projectSelect from '~/project_select'; + +export default projectSelect; diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js new file mode 100644 index 00000000000..2e7a08a369c --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -0,0 +1,7 @@ +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); // eslint-disable-line no-new + new Sidebar(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/index.js b/app/assets/javascripts/pages/dashboard/todos/index/index.js new file mode 100644 index 00000000000..77c23685943 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/todos/index/index.js @@ -0,0 +1,3 @@ +import Todos from './todos'; + +export default () => new Todos(); diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 748caecf153..e976a3d2f1d 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ -import { visitUrl } from './lib/utils/url_utility'; -import UsersSelect from './users_select'; -import { isMetaClick } from './lib/utils/common_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import UsersSelect from '~/users_select'; +import { isMetaClick } from '~/lib/utils/common_utils'; export default class Todos { constructor() { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js index 001faf4be33..821aa7e229f 100644 --- a/app/assets/javascripts/pipelines/pipelines_charts.js +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -6,16 +6,16 @@ document.addEventListener('DOMContentLoaded', () => { const data = { labels: chartScope.labels, datasets: [{ - fillColor: '#7f8fa4', - strokeColor: '#7f8fa4', - pointColor: '#7f8fa4', + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', pointStrokeColor: '#EEE', data: chartScope.totalValues, }, { - fillColor: '#44aa22', - strokeColor: '#44aa22', - pointColor: '#44aa22', + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', pointStrokeColor: '#fff', data: chartScope.successValues, }, diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index ffaafb3ee9e..86c7b56198d 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -6,195 +6,193 @@ // (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // -(function () { - var lastTextareaPreviewed; - var lastTextareaHeight = null; - var markdownPreview; - var previewButtonSelector; - var writeButtonSelector; - - window.MarkdownPreview = (function () { - function MarkdownPreview() {} - - // Minimum number of users referenced before triggering a warning - MarkdownPreview.prototype.referenceThreshold = 10; - MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; - - MarkdownPreview.prototype.ajaxCache = {}; - - MarkdownPreview.prototype.showPreview = function ($form) { - var mdText; - var preview = $form.find('.js-md-preview'); - var url = preview.data('url'); - if (preview.hasClass('md-preview-loading')) { - return; - } - mdText = $form.find('textarea.markdown-area').val(); - - if (mdText.trim().length === 0) { - preview.text(this.emptyMessage); - this.hideReferencedUsers($form); - } else { - preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, url, (function (response) { - var body; - if (response.body.length > 0) { - body = response.body; - } else { - body = this.emptyMessage; - } - - preview.removeClass('md-preview-loading').html(body); - preview.renderGFM(); - this.renderReferencedUsers(response.references.users, $form); - - if (response.references.commands) { - this.renderReferencedCommands(response.references.commands, $form); - } - }).bind(this)); - } - }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { - if (!url) { - return; - } - if (text === this.ajaxCache.text) { - success(this.ajaxCache.response); - return; - } - $.ajax({ - type: 'POST', - url: url, - data: { - text: text - }, - dataType: 'json', - success: (function (response) { - this.ajaxCache = { - text: text, - response: response - }; - success(response); - }).bind(this) - }); - }; - - MarkdownPreview.prototype.hideReferencedUsers = function ($form) { - $form.find('.referenced-users').hide(); - }; - - MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { - var referencedUsers; - referencedUsers = $form.find('.referenced-users'); - if (referencedUsers.length) { - if (users.length >= this.referenceThreshold) { - referencedUsers.show(); - referencedUsers.find('.js-referenced-users-count').text(users.length); - } else { - referencedUsers.hide(); - } - } - }; - - MarkdownPreview.prototype.hideReferencedCommands = function ($form) { - $form.find('.referenced-commands').hide(); - }; - - MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { - var referencedCommands; - referencedCommands = $form.find('.referenced-commands'); - if (commands.length > 0) { - referencedCommands.html(commands); - referencedCommands.show(); +var lastTextareaPreviewed; +var lastTextareaHeight = null; +var markdownPreview; +var previewButtonSelector; +var writeButtonSelector; + +function MarkdownPreview() {} + +// Minimum number of users referenced before triggering a warning +MarkdownPreview.prototype.referenceThreshold = 10; +MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; + +MarkdownPreview.prototype.ajaxCache = {}; + +MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + + if (mdText.trim().length === 0) { + preview.text(this.emptyMessage); + this.hideReferencedUsers($form); + } else { + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; } else { - referencedCommands.html(''); - referencedCommands.hide(); + body = this.emptyMessage; } - }; - - return MarkdownPreview; - }()); - - markdownPreview = new window.MarkdownPreview(); - previewButtonSelector = '.js-md-preview-button'; - writeButtonSelector = '.js-md-write-button'; - lastTextareaPreviewed = null; - const markdownToolbar = $('.md-header-toolbar'); - - $.fn.setupMarkdownPreview = function () { - var $form = $(this); - $form.find('textarea.markdown-area').on('input', function () { - markdownPreview.hideReferencedUsers($form); - }); - }; - - $(document).on('markdown-preview:show', function (e, $form) { - if (!$form) { - return; - } - - lastTextareaPreviewed = $form.find('textarea.markdown-area'); - lastTextareaHeight = lastTextareaPreviewed.height(); - - // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); - // toggle content - $form.find('.md-write-holder').hide(); - $form.find('.md-preview-holder').show(); - markdownToolbar.removeClass('active'); - markdownPreview.showPreview($form); - }); - - $(document).on('markdown-preview:hide', function (e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = null; - - if (lastTextareaHeight) { - $form.find('textarea.markdown-area').height(lastTextareaHeight); - } - - // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); - - // toggle content - $form.find('.md-write-holder').show(); - $form.find('textarea.markdown-area').focus(); - $form.find('.md-preview-holder').hide(); - markdownToolbar.addClass('active'); + preview.removeClass('md-preview-loading').html(body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); - markdownPreview.hideReferencedCommands($form); + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } + }).bind(this)); + } +}; + +MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { + return; + } + if (text === this.ajaxCache.text) { + success(this.ajaxCache.response); + return; + } + $.ajax({ + type: 'POST', + url: url, + data: { + text: text + }, + dataType: 'json', + success: (function (response) { + this.ajaxCache = { + text: text, + response: response + }; + success(response); + }).bind(this) }); - - $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { - var $target; - $target = $(keyboardEvent.target); - if ($target.is('textarea.markdown-area')) { - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - keyboardEvent.preventDefault(); - } else if (lastTextareaPreviewed) { - $target = lastTextareaPreviewed; - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - keyboardEvent.preventDefault(); +}; + +MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); +}; + +MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { + var referencedUsers; + referencedUsers = $form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + referencedUsers.hide(); } + } +}; + +MarkdownPreview.prototype.hideReferencedCommands = function ($form) { + $form.find('.referenced-commands').hide(); +}; + +MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { + var referencedCommands; + referencedCommands = $form.find('.referenced-commands'); + if (commands.length > 0) { + referencedCommands.html(commands); + referencedCommands.show(); + } else { + referencedCommands.html(''); + referencedCommands.hide(); + } +}; + +markdownPreview = new MarkdownPreview(); + +previewButtonSelector = '.js-md-preview-button'; +writeButtonSelector = '.js-md-write-button'; +lastTextareaPreviewed = null; +const markdownToolbar = $('.md-header-toolbar'); + +$.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); +}; + +$(document).on('markdown-preview:show', function (e, $form) { + if (!$form) { + return; + } + + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + + // toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + + // toggle content + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + markdownToolbar.removeClass('active'); + markdownPreview.showPreview($form); +}); + +$(document).on('markdown-preview:hide', function (e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; - $(document).on('click', previewButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:show', [$form]); - }); - - $(document).on('click', writeButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:hide', [$form]); - }); -}()); + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + + // toggle tabs + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + + // toggle content + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + $form.find('.md-preview-holder').hide(); + markdownToolbar.addClass('active'); + + markdownPreview.hideReferencedCommands($form); +}); + +$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } +}); + +$(document).on('click', previewButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:show', [$form]); +}); + +$(document).on('click', writeButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:hide', [$form]); +}); + +export default MarkdownPreview; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index bc907a390d8..d1b3754d4ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -28,7 +28,9 @@ .dropdown-menu, .dropdown-menu-nav { @include set-visible; - min-height: 40px; + min-height: $dropdown-min-height; + max-height: $dropdown-max-height; + overflow: auto; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2d7465401f1..621a4adc0cb 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -260,7 +260,7 @@ } .filtered-search-input-dropdown-menu { - max-height: 260px; + max-height: $dropdown-max-height; max-width: 280px; overflow: auto; diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 3f0268541a4..fab3270b9f5 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -106,10 +106,6 @@ body { } } -.layout-page > .content-wrapper { - min-height: calc(100vh - #{$header-height}); -} - .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f7853909f56..ef1520f1f63 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -334,7 +334,8 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-max-height: 215px; +$dropdown-min-height: 40px; +$dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 60b07537799..1d081b58f62 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -651,15 +651,13 @@ min-width: 0; } - .diff-changed-file-name, - .diff-changed-file-path { + .diff-changed-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .diff-changed-file-path { - direction: rtl; color: $gl-text-color-tertiary; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index e1637618ab2..ae9a8b0182c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -303,7 +303,6 @@ .gutter-toggle { margin-top: 7px; border-left: 1px solid $border-gray-normal; - padding-left: 0; text-align: center; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 05c1033c5f7..a35ebd48887 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -48,7 +48,7 @@ } .dropdown-menu { - max-height: 250px; + max-height: $dropdown-max-height; overflow-y: auto; } @@ -993,3 +993,11 @@ button.mini-pipeline-graph-dropdown-toggle { font-weight: $gl-font-weight-normal; line-height: 1.5; } + +.legend-all { + color: $gl-text-color-secondary; +} + +.legend-success { + color: $green-500; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6f4c678c4b8..61a76d0387a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -322,13 +322,6 @@ } } -.project-repo-buttons { - .project-action-button .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } -} - .split-one { display: inline-table; margin-right: 12px; diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b12ea760668..45f7d29eb05 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -146,6 +146,7 @@ module ApplicationSettingsHelper :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :authorized_keys_enabled, :auto_devops_enabled, :circuitbreaker_access_retries, :circuitbreaker_check_interval, diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 1ce487e6592..0f5fc2823a3 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -226,4 +226,12 @@ module DiffHelper diffs.overflow? end + + def diff_file_path_text(diff_file, max: 60) + path = diff_file.new_path + + return path unless path.size > max && max > 3 + + "...#{path[-(max - 3)..-1]}" + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 253e213af81..8ab338d873d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -261,6 +261,7 @@ class ApplicationSetting < ActiveRecord::Base { after_sign_up_text: nil, akismet_enabled: false, + authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], diff --git a/app/models/repository.rb b/app/models/repository.rb index 9c879e2006b..b36e756c07c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -103,6 +103,10 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end + def create_hooks + Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path) + end + def commit(ref = 'HEAD') return nil unless exists? return ref if ref.is_a?(::Commit) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3e2dbb07a6c..ba4ca88a8a9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -775,6 +775,22 @@ = link_to icon('question-circle'), help_page_path('administration/polling') %fieldset + %legend Performance optimization + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :authorized_keys_enabled do + = f.check_box :authorized_keys_enabled + Write to "authorized_keys" file + .help-block + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + + %fieldset %legend User and IP Rate Limits .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml index 3701e1c0578..c18077bc66f 100644 --- a/app/views/dashboard/projects/_nav.html.haml +++ b/app/views/dashboard/projects/_nav.html.haml @@ -1,4 +1,4 @@ -.top-area +.nav-block %ul.nav-links = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do = link_to s_('DashboardProjects|All'), dashboard_projects_path diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 691d2528022..4e9ea33e675 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,7 +1,7 @@ !!! 5 %html.devise-layout-html = render "layouts/head" - %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } } + %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } } .page-wrap = render "layouts/header/empty" .login-page-broadcast diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index ed6731bde95..8718bb3db1a 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en" } = render "layouts/head" - %body.ui_charcoal.login-page.application.navless + %body.ui_indigo.login-page.application.navless = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index dd473ebe580..325159dd9a7 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -25,7 +25,7 @@ = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8") %span.diff-changed-file-content.append-right-8 %strong.diff-changed-file-name= diff_file.blob.name - %span.diff-changed-file-path.prepend-top-5= diff_file.new_path + %span.diff-changed-file-path.prepend-top-5= diff_file_path_text(diff_file) %span.diff-changed-stats %span.cgreen< +#{diff_file.added_lines} diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index 7a100843f5e..41dc2f6cf9d 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -4,11 +4,11 @@ %h4= _("Pipelines charts") %p - %span.cgreen + %span.legend-success = icon("circle") = s_("Pipeline|success") - %span.cgray + %span.legend-all = icon("circle") = s_("Pipeline|all") diff --git a/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml b/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml new file mode 100644 index 00000000000..0ab765a43b7 --- /dev/null +++ b/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml @@ -0,0 +1,5 @@ +--- +title: "Issue board: fix for dragging an issue to the very bottom in long lists" +merge_request: 16250 +author: David Kuri +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/41744-substitute-ui-charcoal-with-ui-indigo.yml b/changelogs/unreleased/41744-substitute-ui-charcoal-with-ui-indigo.yml new file mode 100644 index 00000000000..593d3741a09 --- /dev/null +++ b/changelogs/unreleased/41744-substitute-ui-charcoal-with-ui-indigo.yml @@ -0,0 +1,5 @@ +--- +title: Substitute deprecated ui_charcoal with new default ui_indigo +merge_request: 16271 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/changes-dropdown-ellipsis.yml b/changelogs/unreleased/changes-dropdown-ellipsis.yml new file mode 100644 index 00000000000..7e3f378cc33 --- /dev/null +++ b/changelogs/unreleased/changes-dropdown-ellipsis.yml @@ -0,0 +1,5 @@ +--- +title: Fixed chanages dropdown ellipsis positioning +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-dashboard-projects-nav-links-height.yml b/changelogs/unreleased/fix-dashboard-projects-nav-links-height.yml new file mode 100644 index 00000000000..2f6a07bb234 --- /dev/null +++ b/changelogs/unreleased/fix-dashboard-projects-nav-links-height.yml @@ -0,0 +1,5 @@ +--- +title: Fix dashboard projects nav links height +merge_request: 16204 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml b/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml new file mode 100644 index 00000000000..e4b1343876a --- /dev/null +++ b/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml @@ -0,0 +1,5 @@ +--- +title: Fixing bug when wiki last version +merge_request: 16197 +author: +type: fixed diff --git a/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml b/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml new file mode 100644 index 00000000000..dca4dec224c --- /dev/null +++ b/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml @@ -0,0 +1,5 @@ +--- +title: Added option to disable commits stats in the commit endpoint +merge_request: 16309 +author: +type: added diff --git a/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml new file mode 100644 index 00000000000..4386c631f59 --- /dev/null +++ b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml @@ -0,0 +1,5 @@ +--- +title: Backport fast database lookup of SSH authorized_keys from EE +merge_request: 16014 +author: +type: added diff --git a/changelogs/unreleased/sh-fix-bare-import-hooks.yml b/changelogs/unreleased/sh-fix-bare-import-hooks.yml new file mode 100644 index 00000000000..deb6c62f738 --- /dev/null +++ b/changelogs/unreleased/sh-fix-bare-import-hooks.yml @@ -0,0 +1,5 @@ +--- +title: Fix hooks not being set up properly for bare import Rake task +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-store-user-in-api-logs.yml b/changelogs/unreleased/sh-store-user-in-api-logs.yml new file mode 100644 index 00000000000..d904dcaf6d3 --- /dev/null +++ b/changelogs/unreleased/sh-store-user-in-api-logs.yml @@ -0,0 +1,5 @@ +--- +title: Save user ID and username in Grape API log (api_json.log) +merge_request: +author: +type: changed diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index f1066f83dd9..0b86cac51a7 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -36,6 +36,26 @@ module Gollum end end end + + module Git + class Git + def tree_entry(commit, path) + pathname = Pathname.new(path) + tmp_entry = nil + + pathname.each_filename do |dir| + tmp_entry = if tmp_entry.nil? + commit.tree[dir] + else + @repo.lookup(tmp_entry[:oid])[dir] + end + + return nil unless tmp_entry + end + tmp_entry + end + end + end end Rails.application.configure do diff --git a/config/webpack.config.js b/config/webpack.config.js index 5f95255334c..95fa79990e2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,5 +1,6 @@ 'use strict'; +var crypto = require('crypto'); var fs = require('fs'); var path = require('path'); var webpack = require('webpack'); @@ -179,15 +180,34 @@ var config = { if (chunk.name) { return chunk.name; } - return chunk.mapModules((m) => { + + const moduleNames = []; + + function collectModuleNames(m) { + // handle ConcatenatedModule which does not have resource nor context set + if (m.modules) { + m.modules.forEach(collectModuleNames); + return; + } + const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages'); + if (m.resource.indexOf(pagesBase) === 0) { - return path.relative(pagesBase, m.resource) + moduleNames.push(path.relative(pagesBase, m.resource) .replace(/\/index\.[a-z]+$/, '') - .replace(/\//g, '__'); + .replace(/\//g, '__')); + } else { + moduleNames.push(path.relative(m.context, m.resource)); } - return path.relative(m.context, m.resource); - }).join('_'); + } + + chunk.forEachModule(collectModuleNames); + + const hash = crypto.createHash('sha256') + .update(moduleNames.join('_')) + .digest('hex'); + + return `${moduleNames[0]}-${hash.substr(0, 6)}`; }), // create cacheable common library bundle for all vue chunks diff --git a/db/migrate/20160301174731_add_fingerprint_index.rb b/db/migrate/20160301174731_add_fingerprint_index.rb new file mode 100644 index 00000000000..f2c3d1ba1ea --- /dev/null +++ b/db/migrate/20160301174731_add_fingerprint_index.rb @@ -0,0 +1,17 @@ +# rubocop:disable all +class AddFingerprintIndex < ActiveRecord::Migration + disable_ddl_transaction! + + DOWNTIME = false + + # https://gitlab.com/gitlab-org/gitlab-ee/issues/764 + def change + args = [:keys, :fingerprint] + + if Gitlab::Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) unless index_exists?(:keys, :fingerprint) + end +end diff --git a/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb new file mode 100644 index 00000000000..1d86a531eb3 --- /dev/null +++ b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddAuthorizedKeysEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :authorized_keys_enabled, :boolean, default: true, allow_null: false + end + + def down + remove_column :application_settings, :authorized_keys_enabled + end +end diff --git a/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb b/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb index 547cc68e10e..fce1829c982 100644 --- a/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb +++ b/db/post_migrate/20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb @@ -15,8 +15,6 @@ class SchedulePopulateMergeRequestMetricsWithEventsData < ActiveRecord::Migratio end def up - merge_requests = MergeRequest.where("id IN (#{updatable_merge_requests_union_sql})").reorder(:id) - say 'Scheduling `PopulateMergeRequestMetricsWithEventsData` jobs' # It will update around 4_000_000 records in batches of 10_000 merge # requests (running between 10 minutes) and should take around 66 hours to complete. @@ -25,7 +23,7 @@ class SchedulePopulateMergeRequestMetricsWithEventsData < ActiveRecord::Migratio # # More information about the updates in `PopulateMergeRequestMetricsWithEventsData` class. # - merge_requests.each_batch(of: BATCH_SIZE) do |relation, index| + MergeRequest.all.each_batch(of: BATCH_SIZE) do |relation, index| range = relation.pluck('MIN(id)', 'MAX(id)').first BackgroundMigrationWorker.perform_in(index * 10.minutes, MIGRATION, range) @@ -37,32 +35,4 @@ class SchedulePopulateMergeRequestMetricsWithEventsData < ActiveRecord::Migratio execute "update merge_request_metrics set latest_closed_by_id = null" execute "update merge_request_metrics set merged_by_id = null" end - - private - - # On staging: - # Planning time: 0.682 ms - # Execution time: 22033.158 ms - # - def updatable_merge_requests_union_sql - metrics_not_exists_clause = - 'NOT EXISTS (SELECT 1 FROM merge_request_metrics WHERE merge_request_metrics.merge_request_id = merge_requests.id)' - - without_metrics_data = <<-SQL.strip_heredoc - merge_request_metrics.merged_by_id IS NULL OR - merge_request_metrics.latest_closed_by_id IS NULL OR - merge_request_metrics.latest_closed_at IS NULL - SQL - - mrs_without_metrics_record = MergeRequest - .where(metrics_not_exists_clause) - .select(:id) - - mrs_without_events_data = MergeRequest - .joins('INNER JOIN merge_request_metrics ON merge_requests.id = merge_request_metrics.merge_request_id') - .where(without_metrics_data) - .select(:id) - - Gitlab::SQL::Union.new([mrs_without_metrics_record, mrs_without_events_data]).to_sql - end end diff --git a/db/schema.rb b/db/schema.rb index e6a2ea4c862..a16f756ccfb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -154,6 +154,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_fast", default: 10, null: false + t.boolean "authorized_keys_enabled", default: true, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md new file mode 100644 index 00000000000..835ed8c8006 --- /dev/null +++ b/doc/administration/operations/fast_ssh_key_lookup.md @@ -0,0 +1,170 @@ +# Fast lookup of authorized SSH keys in the database + +Regular SSH operations become slow as the number of users grows because OpenSSH +searches for a key to authorize a user via a linear search. In the worst case, +such as when the user is not authorized to access GitLab, OpenSSH will scan the +entire file to search for a key. This can take significant time and disk I/O, +which will delay users attempting to push or pull to a repository. Making +matters worse, if users add or remove keys frequently, the operating system may +not be able to cache the `authorized_keys` file, which causes the disk to be +accessed repeatedly. + +GitLab Shell solves this by providing a way to authorize SSH users via a fast, +indexed lookup in the GitLab database. This page describes how to enable the fast +lookup of authorized SSH keys. + +> **Warning:** OpenSSH version 6.9+ is required because +`AuthorizedKeysCommand` must be able to accept a fingerprint. These +instructions will break installations using older versions of OpenSSH, such as +those included with CentOS 6 as of September 2017. If you want to use this +feature for CentOS 6, follow [the instructions on how to build and install a custom OpenSSH package](#compiling-a-custom-version-of-openssh-for-centos-6) before continuing. + +## Setting up fast lookup via GitLab Shell + +GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup +to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to +check whether the user is authorized to access GitLab. + +Add the following to your `sshd_config` file. This is usuaully located at +`/etc/ssh/sshd_config`, but it will be `/assets/sshd_config` if you're using +Omnibus Docker: + +``` +AuthorizedKeysCommand /opt/embedded/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k +AuthorizedKeysCommandUser git +``` + +Reload OpenSSH: + +```bash +# Debian or Ubuntu installations +sudo service ssh reload + +# CentOS installations +sudo service sshd reload +``` + +Confirm that SSH is working by removing your user's SSH key in the UI, adding a +new one, and attempting to pull a repo. + +> **Warning:** Do not disable writes until SSH is confirmed to be working +perfectly, because the file will quickly become out-of-date. + +In the case of lookup failures (which are not uncommon), the `authorized_keys` +file will still be scanned. So git SSH performance will still be slow for many +users as long as a large file exists. + +You can disable any more writes to the `authorized_keys` file by unchecking +`Write to "authorized_keys" file` in the Application Settings of your GitLab +installation. + +![Write to authorized keys setting](img/write_to_authorized_keys_setting.png) + +Again, confirm that SSH is working by removing your user's SSH key in the UI, +adding a new one, and attempting to pull a repo. + +Then you can backup and delete your `authorized_keys` file for best performance. + +## How to go back to using the `authorized_keys` file + +This is a brief overview. Please refer to the above instructions for more context. + +1. [Rebuild the `authorized_keys` file](../raketasks/maintenance.md#rebuild-authorized_keys-file) +1. Enable writes to the `authorized_keys` file in Application Settings +1. Remove the `AuthorizedKeysCommand` lines from `/etc/ssh/sshd_config` or from `/assets/sshd_config` if you are using Omnibus Docker. +1. Reload sshd: `sudo service sshd reload` +1. Remove the `/opt/gitlab-shell/authorized_keys` file + +## Compiling a custom version of OpenSSH for CentOS 6 + +Building a custom version of OpenSSH is not necessary for Ubuntu 16.04 users, +since Ubuntu 16.04 ships with OpenSSH 7.2. + +It is also unnecessary for CentOS 7.4 users, as that version ships with +OpenSSH 7.4. If you are using CentOS 7.0 - 7.3, we strongly recommend that you +upgrade to CentOS 7.4 instead of following this procedure. This should be as +simple as running `yum update`. + +CentOS 6 users must build their own OpenSSH package to enable SSH lookups via +the database. The following instructions can be used to build OpenSSH 7.5: + +1. First, download the package and install the required packages: + + ``` + sudo su - + cd /tmp + curl --remote-name https://mirrors.evowise.com/pub/OpenBSD/OpenSSH/portable/openssh-7.5p1.tar.gz + tar xzvf openssh-7.5p1.tar.gz + yum install rpm-build gcc make wget openssl-devel krb5-devel pam-devel libX11-devel xmkmf libXt-devel + ``` + +3. Prepare the build by copying files to the right place: + + ``` + mkdir -p /root/rpmbuild/{SOURCES,SPECS} + cp ./openssh-7.5p1/contrib/redhat/openssh.spec /root/rpmbuild/SPECS/ + cp openssh-7.5p1.tar.gz /root/rpmbuild/SOURCES/ + cd /root/rpmbuild/SPECS + ``` + +3. Next, set the spec settings properly: + + ``` + sed -i -e "s/%define no_gnome_askpass 0/%define no_gnome_askpass 1/g" openssh.spec + sed -i -e "s/%define no_x11_askpass 0/%define no_x11_askpass 1/g" openssh.spec + sed -i -e "s/BuildPreReq/BuildRequires/g" openssh.spec + ``` + +3. Build the RPMs: + + ``` + rpmbuild -bb openssh.spec + ``` + +4. Ensure the RPMs were built: + + ``` + ls -al /root/rpmbuild/RPMS/x86_64/ + ``` + + You should see something as the following: + + ``` + total 1324 + drwxr-xr-x. 2 root root 4096 Jun 20 19:37 . + drwxr-xr-x. 3 root root 19 Jun 20 19:37 .. + -rw-r--r--. 1 root root 470828 Jun 20 19:37 openssh-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 490716 Jun 20 19:37 openssh-clients-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 17020 Jun 20 19:37 openssh-debuginfo-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 367516 Jun 20 19:37 openssh-server-7.5p1-1.x86_64.rpm + ``` + +5. Install the packages. OpenSSH packages will replace `/etc/pam.d/sshd` + with its own version, which may prevent users from logging in, so be sure + that the file is backed up and restored after installation: + + ``` + timestamp=$(date +%s) + cp /etc/pam.d/sshd pam-ssh-conf-$timestamp + rpm -Uvh /root/rpmbuild/RPMS/x86_64/*.rpm + yes | cp pam-ssh-conf-$timestamp /etc/pam.d/sshd + ``` + +6. Verify the installed version. In another window, attempt to login to the server: + + ``` + ssh -v <your-centos-machine> + ``` + + You should see a line that reads: "debug1: Remote protocol version 2.0, remote software version OpenSSH_7.5" + + If not, you may need to restart sshd (e.g. `systemctl restart sshd.service`). + +7. *IMPORTANT!* Open a new SSH session to your server before exiting to make + sure everything is working! If you need to downgrade, simple install the + older package: + + ``` + # Only run this if you run into a problem logging in + yum downgrade openssh-server openssh openssh-clients + ``` diff --git a/doc/administration/operations/img/write_to_authorized_keys_setting.png b/doc/administration/operations/img/write_to_authorized_keys_setting.png Binary files differnew file mode 100644 index 00000000000..232765f1917 --- /dev/null +++ b/doc/administration/operations/img/write_to_authorized_keys_setting.png diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index 320d71a9527..5655b7efec6 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -13,4 +13,5 @@ by GitLab to another file system or another server. that to prioritize important jobs. - [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller to restart Sidekiq. -- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
\ No newline at end of file +- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. +- [Speed up SSH operations](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database. diff --git a/doc/administration/operations/speed_up_ssh.md b/doc/administration/operations/speed_up_ssh.md new file mode 100644 index 00000000000..89265b3018b --- /dev/null +++ b/doc/administration/operations/speed_up_ssh.md @@ -0,0 +1 @@ +This document was moved to [another location](fast_ssh_key_lookup.md). diff --git a/doc/api/commits.md b/doc/api/commits.md index c9b72d4a1dd..63554c63057 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -159,6 +159,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | +| `stats` | boolean | no | Include commit stats. Default is true | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 03b32577872..5fb25e40ed7 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -113,7 +113,7 @@ GET /projects/:id/repository/archive Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `sha` (optional) - The commit SHA to download defaults to the tip of the default branch +- `sha` (optional) - The commit SHA to download. A tag, branch reference or sha can be used. This defaults to the tip of the default branch if not specified ## Compare branches, tags or commits diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index 6a5821762cc..f919ed3c797 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -16,7 +16,8 @@ codequality: - docker:dind script: - docker pull codeclimate/codeclimate - - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json || true + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true artifacts: paths: [codeclimate.json] ``` diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index df0e1521150..b8df0bfba20 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -181,7 +181,7 @@ before_script: ## Assuming you created the SSH_KNOWN_HOSTS variable, uncomment the ## following two lines. ## - - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts' + - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts ## diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 54029e00507..d1ba7d3dfc3 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -133,8 +133,6 @@ Usage: /etc/init.d/postgresql {start|stop|restart|reload|force-reload|status} [v ### Log locations of the services -Note: `/home/git/` is shorthand for `/home/git`. - gitlabhq (includes Unicorn and Sidekiq logs) - `/home/git/gitlab/log/` contains `application.log`, `production.log`, `sidekiq.log`, `unicorn.stdout.log`, `githost.log` and `unicorn.stderr.log` normally. diff --git a/doc/development/changelog.md b/doc/development/changelog.md index 48cffc0dd18..18f4177a5e5 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -127,7 +127,7 @@ type: If you're working on the GitLab EE repository, the entry will be added to `changelogs/unreleased-ee/` instead. -#### Arguments +### Arguments | Argument | Shorthand | Purpose | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- | diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md index 77b308c4a43..86a8b4135af 100644 --- a/doc/development/fe_guide/style_guide_scss.md +++ b/doc/development/fe_guide/style_guide_scss.md @@ -216,7 +216,7 @@ If you want a line or set of lines to be ignored by the linter, you can use ```scss // This lint rule is disabled because the class name comes from a gem. // scss-lint:disable SelectorFormat -.ui_charcoal { +.ui_indigo { background-color: #333; } // scss-lint:enable SelectorFormat diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index d5619c7b563..5f14d232cb1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -2,9 +2,6 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1. -CAUTION: **Warning:** -The Cluster integration is currently in **Beta**. - With a cluster associated to your project, you can use Review Apps, deploy your applications, run your pipelines, and much more, in an easy way. diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index c63ea1316fe..ecdd83ce8f0 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -47,4 +47,8 @@ Irker accepts channel names of the form `chan` and `#chan`, both for the case, `Aorimn` is treated as a nick and no more as a channel name. Irker can also join password-protected channels. Users need to append -`?key=thesecretpassword` to the chan name. +`?key=thesecretpassword` to the chan name. When using this feature remember to +**not** put the `#` sign in front of the channel name; failing to do so will +result on irker joining a channel literally named `#chan?key=password` henceforth +leaking the channel key through the `/whois` IRC command (depending on IRC server +configuration). This is due to a long standing irker bug. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index eafdd28071d..82175c70e49 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -54,6 +54,12 @@ Below are described the supported events. Triggered when you push to the repository except when pushing tags. +> **Note:** When more than 20 commits are pushed at once, the `commits` web hook + attribute will only contain the first 20 for performance reasons. Loading + detailed commit data is expensive. Note that despite only 20 commits being + present in the `commits` attribute, the `total_commits_count` attribute will + contain the actual total. + **Request header**: ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index e0d14281c96..ae161efb358 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -13,7 +13,8 @@ module API formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, include: [ GrapeLogging::Loggers::FilterParameters.new, - GrapeLogging::Loggers::ClientEnv.new + GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::UserLogger.new ] allow_access_with_scope :api diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 38e05074353..d8fd6a6eb06 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -82,13 +82,14 @@ module API end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + optional :stats, type: Boolean, default: true, desc: 'Include commit stats' end get ':id/repository/commits/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present commit, with: Entities::CommitDetail + present commit, with: Entities::CommitDetail, stats: params[:stats] end desc 'Get the diff for a specific commit of a project' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index bd0c54a1b04..f574858be02 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -278,7 +278,7 @@ module API end class CommitDetail < Commit - expose :stats, using: Entities::CommitStats + expose :stats, using: Entities::CommitStats, if: :stats expose :status expose :last_pipeline, using: 'API::Entities::PipelineBasic' end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index bf388163ec8..d6ce368efd5 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -5,6 +5,7 @@ module API SUDO_HEADER = "HTTP_SUDO".freeze SUDO_PARAM = :sudo + API_USER_ENV = 'gitlab.api.user'.freeze def declared_params(options = {}) options = { include_parent_namespaces: false }.merge(options) @@ -48,10 +49,16 @@ module API validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo? + save_current_user_in_env(@current_user) if @current_user + @current_user end # rubocop:enable Gitlab/ModuleWithInstanceVariables + def save_current_user_in_env(user) + env[API_USER_ENV] = { user_id: user.id, username: user.username } + end + def sudo? initial_current_user != current_user end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 79b302aae70..8bf53939751 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -82,6 +82,18 @@ module API end # + # Get a ssh key using the fingerprint + # + get "/authorized_keys" do + fingerprint = params.fetch(:fingerprint) do + Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint + end + key = Key.find_by(fingerprint: fingerprint) + not_found!("Key") if key.nil? + present key, with: Entities::SSHKey + end + + # # Discover user by ssh key or user id # get "/discover" do diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 0ef26aa696a..4f6ea8f502e 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -71,13 +71,14 @@ module API end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + optional :stats, type: Boolean, default: true, desc: 'Include commit stats' end get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! "Commit" unless commit - present commit, with: ::API::Entities::CommitDetail + present commit, with: ::API::Entities::CommitDetail, stats: params[:stats] end desc 'Get the diff for a specific commit of a project' do diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb index 709a901aa77..884a3de8f62 100644 --- a/lib/gitlab/bare_repository_import/importer.rb +++ b/lib/gitlab/bare_repository_import/importer.rb @@ -63,6 +63,7 @@ module Gitlab log " * Created #{project.name} (#{project_full_path})".color(:green) project.write_repository_config + project.repository.create_hooks ProjectCacheWorker.perform_async(project.id) else diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index cba638c06db..976fa1ddfe6 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -41,36 +41,6 @@ module Gitlab io.read end - def rm_project - logger.info "Removing repository <#{repository_absolute_path}>." - FileUtils.rm_rf(repository_absolute_path) - end - - # Move repository from one directory to another - # - # Example: gitlab/gitlab-ci.git -> randx/six.git - # - # Won't work if target namespace directory does not exist - # - def mv_project(new_path) - new_absolute_path = File.join(shard_path, new_path) - - # verify that the source repo exists - unless File.exist?(repository_absolute_path) - logger.error "mv-project failed: source path <#{repository_absolute_path}> does not exist." - return false - end - - # ...and that the target repo does not exist - if File.exist?(new_absolute_path) - logger.error "mv-project failed: destination path <#{new_absolute_path}> already exists." - return false - end - - logger.info "Moving repository from <#{repository_absolute_path}> to <#{new_absolute_path}>." - FileUtils.mv(repository_absolute_path, new_absolute_path) - end - # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb index 559a901b9a3..e58f641d69a 100644 --- a/lib/gitlab/gitaly_client/remote_service.rb +++ b/lib/gitlab/gitaly_client/remote_service.rb @@ -7,10 +7,12 @@ module Gitlab @storage = repository.storage end - def add_remote(name, url, mirror_refmap) + def add_remote(name, url, mirror_refmaps) request = Gitaly::AddRemoteRequest.new( - repository: @gitaly_repo, name: name, url: url, - mirror_refmap: mirror_refmap.to_s + repository: @gitaly_repo, + name: name, + url: url, + mirror_refmaps: Array.wrap(mirror_refmaps).map(&:to_s) ) GitalyClient.call(@storage, :remote_service, :add_remote, request) diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index d43d80da960..66006f5dc5b 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -43,8 +43,11 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request) end - def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false) - request = Gitaly::FetchRemoteRequest.new(repository: @gitaly_repo, remote: remote, force: forced, no_tags: no_tags) + def fetch_remote(remote, ssh_auth:, forced:, no_tags:, timeout:) + request = Gitaly::FetchRemoteRequest.new( + repository: @gitaly_repo, remote: remote, force: forced, + no_tags: no_tags, timeout: timeout + ) if ssh_auth&.ssh_import? if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present? diff --git a/lib/gitlab/grape_logging/loggers/user_logger.rb b/lib/gitlab/grape_logging/loggers/user_logger.rb new file mode 100644 index 00000000000..fa172861967 --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/user_logger.rb @@ -0,0 +1,18 @@ +# This grape_logging module (https://github.com/aserafin/grape_logging) makes it +# possible to log the user who performed the Grape API action by retrieving +# the user context from the request environment. +module Gitlab + module GrapeLogging + module Loggers + class UserLogger < ::GrapeLogging::Loggers::Base + def parameters(request, _) + params = request.env[::API::Helpers::API_USER_ENV] + + return {} unless params + + params.slice(:user_id, :username) + end + end + end + end +end diff --git a/lib/gitlab/insecure_key_fingerprint.rb b/lib/gitlab/insecure_key_fingerprint.rb new file mode 100644 index 00000000000..f85b6e9197f --- /dev/null +++ b/lib/gitlab/insecure_key_fingerprint.rb @@ -0,0 +1,23 @@ +module Gitlab + # + # Calculates the fingerprint of a given key without using + # openssh key validations. For this reason, only use + # for calculating the fingerprint to find the key with it. + # + # DO NOT use it for checking the validity of a ssh key. + # + class InsecureKeyFingerprint + attr_accessor :key + + # + # Gets the base64 encoded string representing a rsa or dsa key + # + def initialize(key_base64) + @key = key_base64 + end + + def fingerprint + OpenSSL::Digest::MD5.hexdigest(Base64.decode64(@key)).scan(/../).join(':') + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 2c7b8af83f2..0002c7da8f1 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -37,7 +37,7 @@ module Gitlab end def environment_name_regex_chars - 'a-zA-Z0-9_/\\$\\{\\}\\. -' + 'a-zA-Z0-9_/\\$\\{\\}\\. \\-' end def environment_name_regex diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 564047bbd34..f4a41dc3eda 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -128,7 +128,7 @@ module Gitlab def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false) gitaly_migrate(:fetch_remote) do |is_enabled| if is_enabled - repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags) + repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout) else storage_path = Gitlab.config.repositories.storages[repository.storage]["path"] local_fetch_remote(storage_path, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags) @@ -136,7 +136,10 @@ module Gitlab end end - # Move repository + # Move repository reroutes to mv_directory which is an alias for + # mv_namespace. Given the underlying implementation is a move action, + # indescriminate of what the folders might be. + # # storage - project's storage path # path - project disk path # new_path - new project disk path @@ -146,7 +149,9 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def mv_repository(storage, path, new_path) - gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") + return false if path.empty? || new_path.empty? + + !!mv_directory(storage, "#{path}.git", "#{new_path}.git") end # Fork repository to new path @@ -164,7 +169,9 @@ module Gitlab .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") end - # Remove repository from file system + # Removes a repository from file system, using rm_diretory which is an alias + # for rm_namespace. Given the underlying implementation removes the name + # passed as second argument on the passed storage. # # storage - project's storage path # name - project disk path @@ -174,7 +181,12 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def remove_repository(storage, name) - gitlab_projects(storage, "#{name}.git").rm_project + return false if name.empty? + + !!rm_directory(storage, "#{name}.git") + rescue ArgumentError => e + Rails.logger.warn("Repository does not exist: #{e} at: #{name}.git") + false end # Add new key to gitlab-shell @@ -183,6 +195,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'add-key', key_id, self.class.strip_key(key_content)]) end @@ -192,6 +206,8 @@ module Gitlab # Ex. # batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") } def batch_add_keys(&block) + return unless self.authorized_keys_enabled? + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io| yield(KeyAdder.new(io)) end @@ -202,10 +218,11 @@ module Gitlab # Ex. # remove_key("key-342", "sha-rsa ...") # - def remove_key(key_id, key_content) + def remove_key(key_id, key_content = nil) + return unless self.authorized_keys_enabled? + args = [gitlab_shell_keys_path, 'rm-key', key_id] args << key_content if key_content - gitlab_shell_fast_execute(args) end @@ -215,9 +232,62 @@ module Gitlab # remove_all_keys # def remove_all_keys + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) end + # Remove ssh keys from gitlab shell that are not in the DB + # + # Ex. + # remove_keys_not_found_in_db + # + def remove_keys_not_found_in_db + return unless self.authorized_keys_enabled? + + Rails.logger.info("Removing keys not found in DB") + + batch_read_key_ids do |ids_in_file| + ids_in_file.uniq! + keys_in_db = Key.where(id: ids_in_file) + + next unless ids_in_file.size > keys_in_db.count # optimization + + ids_to_remove = ids_in_file - keys_in_db.pluck(:id) + ids_to_remove.each do |id| + Rails.logger.info("Removing key-#{id} not found in DB") + remove_key("key-#{id}") + end + end + end + + # Iterate over all ssh key IDs from gitlab shell, in batches + # + # Ex. + # batch_read_key_ids { |batch| keys = Key.where(id: batch) } + # + def batch_read_key_ids(batch_size: 100, &block) + return unless self.authorized_keys_enabled? + + list_key_ids do |key_id_stream| + key_id_stream.lazy.each_slice(batch_size) do |lines| + key_ids = lines.map { |l| l.chomp.to_i } + yield(key_ids) + end + end + end + + # Stream all ssh key IDs from gitlab shell, separated by newlines + # + # Ex. + # list_key_ids + # + def list_key_ids(&block) + return unless self.authorized_keys_enabled? + + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys list-key-ids), &block) + end + # Add empty directory for storing repositories # # Ex. @@ -255,6 +325,7 @@ module Gitlab rescue GRPC::InvalidArgument => e raise ArgumentError, e.message end + alias_method :rm_directory, :rm_namespace # Move namespace directory inside repositories storage # @@ -274,6 +345,7 @@ module Gitlab rescue GRPC::InvalidArgument false end + alias_method :mv_directory, :mv_namespace def url_to_repo(path) Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git" @@ -333,6 +405,14 @@ module Gitlab File.join(gitlab_shell_path, 'bin', 'gitlab-keys') end + def authorized_keys_enabled? + # Return true if nil to ensure the authorized_keys methods work while + # fixing the authorized_keys file during migration. + return true if Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled.nil? + + Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled + end + private def gitlab_projects(shard_path, disk_path) diff --git a/package.json b/package.json index 78bc8e847c1..4759ae76817 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "autosize": "^4.0.0", "axios": "^0.17.1", - "axios-mock-adapter": "^1.10.0", "babel-core": "^6.26.0", "babel-eslint": "^8.0.2", "babel-loader": "^7.1.2", @@ -88,6 +87,7 @@ }, "devDependencies": { "@gitlab-org/gitlab-svgs": "^1.5.0", + "axios-mock-adapter": "^1.10.0", "babel-plugin-istanbul": "^4.1.5", "eslint": "^3.18.0", "eslint-config-airbnb-base": "^10.0.1", diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index f9c31ac61d8..15cbe36ae76 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -266,4 +266,14 @@ describe DiffHelper do end end end + + context '#diff_file_path_text' do + it 'returns full path by default' do + expect(diff_file_path_text(diff_file)).to eq(diff_file.new_path) + end + + it 'returns truncated path' do + expect(diff_file_path_text(diff_file, max: 10)).to eq("...open.rb") + end + end end diff --git a/spec/initializers/gollum_spec.rb b/spec/initializers/gollum_spec.rb new file mode 100644 index 00000000000..adf824a8947 --- /dev/null +++ b/spec/initializers/gollum_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'gollum' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + let(:gollum_wiki) { Gollum::Wiki.new(wiki.repository.path) } + + before do + create_page(page_name, 'content1') + end + + after do + destroy_page(page_name) + end + + context 'with simple paths' do + let(:page_name) { 'page1' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo/#{page_name}")).to be_nil + end + end + + context 'with complex paths' do + let(:page_name) { '/foo/bar/page2' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo1/bar/page2")).to be_nil + expect(tree_entry("foo/bar1/page2")).to be_nil + end + end + + def tree_entry(name) + gollum_wiki.repo.git.tree_entry(wiki_commits[0].commit, name + '.md') + end + + def wiki_commits + gollum_wiki.repo.commits + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + end + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(name) + page = wiki.find_page(name).page + wiki.delete_page(page, "test commit") + end +end diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 7c5888b6d82..b7cc3a8813e 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -154,6 +154,18 @@ describe('Board list component', () => { }); }); + it('sets data attribute with invalid id', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), + ).toBe('-1'); + + done(); + }); + }); + it('shows how many more issues to load', (done) => { component.showCount = true; component.list.issuesSize = 20; diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 2f02c11482f..9d6ea3781bc 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -19,17 +19,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { + + it('submits an ajax request on tasklist:changed', (done) => { + spyOn(jQuery, 'ajax').and.callFake((req) => { expect(req.type).toBe('PATCH'); expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); - return expect(req.data.merge_request.description).not.toBe(null); + expect(req.data.merge_request.description).not.toBe(null); + done(); }); - return $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); describe('class constructor', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax').and.stub(); + }); + it('calls .initCloseReopenReport', () => { spyOn(IssuablesHelper, 'initCloseReopenReport'); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 59e16f0786e..35871dddf89 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,5 @@ import * as urlUtils from '~/lib/utils/url_utility'; -import Todos from '~/todos'; +import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; describe('Todos', () => { diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 153b5c51cbf..c63f15e5880 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -32,7 +32,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.innerHTML).not.toBeDefined(); + expect(component.$el.childNodes.length).toEqual(0); }); describe('prev button', () => { diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index b5d86df09d2..f302e412a6e 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -74,14 +74,18 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do importer.create_project_if_needed end - it 'creates the Git repo in disk' do + it 'creates the Git repo on disk with the proper symlink for hooks' do create_bare_repository("#{project_path}.git") importer.create_project_if_needed project = Project.find_by_full_path(project_path) + repo_path = File.join(project.repository_storage_path, project.disk_path + '.git') + hook_path = File.join(repo_path, 'hooks') - expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git')) + expect(File).to exist(repo_path) + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end context 'hashed storage enabled' do diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb index a798b188a0d..beef843537d 100644 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -25,51 +25,6 @@ describe Gitlab::Git::GitlabProjects do it { expect(gl_projects.logger).to eq(logger) } end - describe '#mv_project' do - let(:new_repo_path) { File.join(tmp_repos_path, 'repo.git') } - - it 'moves a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - - message = "Moving repository from <#{tmp_repo_path}> to <#{new_repo_path}>." - expect(logger).to receive(:info).with(message) - - expect(gl_projects.mv_project('repo.git')).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - expect(File.exist?(new_repo_path)).to be_truthy - end - - it "fails if the source path doesn't exist" do - expected_source_path = File.join(tmp_repos_path, 'bad-src.git') - expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.") - - result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git') - expect(result).to be_falsy - end - - it 'fails if the destination path already exists' do - FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git')) - - expected_distination_path = File.join(tmp_repos_path, 'already-exists.git') - message = "mv-project failed: destination path <#{expected_distination_path}> already exists." - expect(logger).to receive(:error).with(message) - - expect(gl_projects.mv_project('already-exists.git')).to be_falsy - end - end - - describe '#rm_project' do - it 'removes a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - expect(logger).to receive(:info).with("Removing repository <#{tmp_repo_path}>.") - - expect(gl_projects.rm_project).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - end - end - describe '#push_branches' do let(:remote_name) { 'remote-name' } let(:branch_name) { 'master' } diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb new file mode 100644 index 00000000000..6532579b1c9 --- /dev/null +++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::InsecureKeyFingerprint do + let(:key) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn' \ + '1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qk' \ + 'r8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMg' \ + 'Jw0=' + end + + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(described_class.new(key.split[1]).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 68a57826647..8b54d72d6f7 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Regex do it { is_expected.not_to match('?gitlab') } end - describe '.environment_slug_regex' do + describe '.environment_name_regex' do subject { described_class.environment_name_regex } it { is_expected.to match('foo') } @@ -24,6 +24,7 @@ describe Gitlab::Regex do it { is_expected.to match('foo.1') } it { is_expected.not_to match('9&foo') } it { is_expected.not_to match('foo-^') } + it { is_expected.not_to match('!!()()') } end describe '.environment_slug_regex' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 81d9e6a8f82..2b61ce38418 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -4,6 +4,7 @@ require 'stringio' describe Gitlab::Shell do set(:project) { create(:project, :repository) } + let(:repository) { project.repository } let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } let(:gitlab_projects) { double('gitlab_projects') } @@ -51,6 +52,311 @@ describe Gitlab::Shell do end end + describe '#add_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + end + + describe '#batch_add_keys' do + context 'when authorized_keys_enabled is true' do + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + end + + describe '#remove_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when key content is not given' do + it 'calls rm-key with only one argument' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123'] + ) + + gitlab_shell.remove_key('key-123') + end + end + end + + describe '#remove_all_keys' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'clear'] + ) + + gitlab_shell.remove_all_keys + end + end + end + + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + expect(find_in_authorized_keys_file(9876)).to be_truthy + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + expect(find_in_authorized_keys_file(9876)).to be_falsey + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + end + end + + context 'when keys there are duplicate keys in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + + it 'does not run remove more than once per key (in a batch)' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234').once + gitlab_shell.remove_keys_not_found_in_db + end + end + + context 'when keys there are duplicate keys in the file that ARE in the DB' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end + + it 'does not remove the key' do + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(@key.id)).to be_truthy + end + + it 'does not need to run a SELECT query for that batch, on account of that key' do + expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) + gitlab_shell.remove_keys_not_found_in_db + end + end + + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + end + end + end + + describe '#batch_read_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'iterates over the key IDs in the file, in batches' do + loop_count = 0 + first_batch = [1, 2] + second_batch = [3, 4] + + gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| + expected = (loop_count == 0 ? first_batch : second_batch) + expect(batch).to eq(expected) + loop_count += 1 + end + end + end + end + + describe '#list_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'outputs the key IDs in the file, separated by newlines' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) + end + end + + context 'when there are no keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + end + + it 'outputs nothing, not even an empty string' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq([]) + end + end + end + describe Gitlab::Shell::KeyAdder do describe '#add_key' do it 'removes trailing garbage' do @@ -96,17 +402,6 @@ describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - describe '#add_repository' do shared_examples '#add_repository' do let(:repository_storage) { 'default' } @@ -148,32 +443,44 @@ describe Gitlab::Shell do end describe '#remove_repository' do - subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) } + let!(:project) { create(:project, :repository) } + let(:disk_path) { "#{project.disk_path}.git" } it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:rm_project) { true } + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(true) - is_expected.to be_truthy + expect(gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)).to be(true) + + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) end - it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:rm_project) { false } + it 'keeps the namespace directory' do + gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) - is_expected.to be_falsy + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) + expect(gitlab_shell.exists?(project.repository_storage_path, project.disk_path.gsub(project.name, ''))).to be(true) end end describe '#mv_repository' do + let!(:project2) { create(:project, :repository) } + it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true } + old_path = project2.disk_path + new_path = "project/new_path" + + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(true) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(false) - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy + expect(gitlab_shell.mv_repository(project2.repository_storage_path, old_path, new_path)).to be_truthy + + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(false) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(true) end it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false } - - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy + expect(gitlab_shell.mv_repository(project2.repository_storage_path, project2.disk_path, '')).to be_falsy + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{project2.disk_path}.git")).to be(true) end end @@ -201,8 +508,6 @@ describe Gitlab::Shell do end shared_examples 'fetch_remote' do |gitaly_on| - let(:repository) { project.repository } - def fetch_remote(ssh_auth = nil) gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth) end @@ -325,6 +630,23 @@ describe Gitlab::Shell do describe '#fetch_remote gitaly' do it_should_behave_like 'fetch_remote', true + + context 'gitaly call' do + let(:remote_name) { 'remote-name' } + let(:ssh_auth) { double(:ssh_auth) } + + subject do + gitlab_shell.fetch_remote(repository.raw_repository, remote_name, + forced: true, no_tags: true, ssh_auth: ssh_auth) + end + + it 'passes the correct params to the gitaly service' do + expect(repository.gitaly_repository_client).to receive(:fetch_remote) + .with(remote_name, ssh_auth: ssh_auth, forced: true, no_tags: true, timeout: timeout) + + subject + end + end end describe '#import_repository' do @@ -396,4 +718,12 @@ describe Gitlab::Shell do end end end + + def find_in_authorized_keys_file(key_id) + gitlab_shell.batch_read_key_ids do |ids| + return true if ids.include?(key_id) + end + + false + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c0db2c1b386..edd981752d9 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -412,6 +412,28 @@ describe Repository do end end + describe '#create_hooks' do + let(:hook_path) { File.join(repository.path_to_repo, 'hooks') } + + it 'symlinks the global hooks directory' do + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + + it 'replaces existing symlink with the right directory' do + FileUtils.mkdir_p(hook_path) + + expect(File.symlink?(hook_path)).to be false + + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + end + describe "#create_dir" do it "commits a change that creates a new directory" do expect do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 0d2bd3207c0..34db50dc082 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -512,6 +512,31 @@ describe API::Commits do end end + context 'when stat param' do + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end + context 'when unauthenticated', 'and project is public' do let(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 0462f494e15..837389451e8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -68,6 +68,12 @@ describe API::Helpers do end it { is_expected.to eq(user) } + + it 'sets the environment with data of the current user' do + subject + + expect(env[API::Helpers::API_USER_ENV]).to eq({ user_id: subject.id, username: subject.username }) + end end context "HEAD request" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 7b25047ea8f..2783c51b8df 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -192,6 +192,54 @@ describe API::Internal do end end + describe "GET /internal/authorized_keys" do + context "using an existing key's fingerprint" do + it "finds the key" do + get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + end + + context "non existing key's fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "no:t-:va:li:d0", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "using a partial fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "#{key.fingerprint[0..5]}%", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "sending the key" do + it "finds the key" do + get(api('/internal/authorized_keys'), key: key.key.split[1], secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + + it "returns 404 with a partial key" do + get(api('/internal/authorized_keys'), key: key.key.split[1][0...-3], secret_token: secret_token) + + expect(response.status).to eq(404) + end + + it "returns 404 with an not valid base64 string" do + get(api('/internal/authorized_keys'), key: "whatever!", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + end + describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do context "access granted" do around do |example| diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 8b115e01f47..34c543bffe8 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -403,6 +403,33 @@ describe API::V3::Commits do expect(response).to have_gitlab_http_status(200) expect(json_response['status']).to eq("created") end + + context 'when stat param' do + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get v3_api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get v3_api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get v3_api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end end context "unauthorized user" do diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb new file mode 100644 index 00000000000..6b222af454d --- /dev/null +++ b/spec/workers/gitlab_shell_worker_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe GitlabShellWorker do + let(:worker) { described_class.new } + + describe '#perform with add_key' do + it 'calls add_key on Gitlab::Shell' do + expect_any_instance_of(Gitlab::Shell).to receive(:add_key).with('foo', 'bar') + worker.perform(:add_key, 'foo', 'bar') + end + end +end |