diff options
364 files changed, 4943 insertions, 1786 deletions
diff --git a/.flayignore b/.flayignore index b63ce4c4df0..acac0ce14c9 100644 --- a/.flayignore +++ b/.flayignore @@ -5,3 +5,4 @@ app/policies/project_policy.rb app/models/concerns/relative_positioning.rb app/workers/stuck_merge_jobs_worker.rb lib/gitlab/redis/*.rb +lib/gitlab/gitaly_client/operation_service.rb diff --git a/.gitignore b/.gitignore index 3baf640a9c3..4933575332b 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,5 @@ eslint-report.html /.gitlab_workhorse_secret /webpack-report/ /locale/**/LC_MESSAGES +/locale/**/*.time_stamp /.rspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 3321ace28fc..15d9117976a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,17 @@ entry. - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - [BUGIFX] Improves subgroup creation permissions. !13418 +## 9.5.7 (2017-10-03) + +- Fix gitlab rake:import:repos task. + +## 9.5.6 (2017-09-29) + +- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242 +- [FIXED] Fix errors thrown in merge request widget with external CI service/integration. +- [FIXED] Update x/x discussions resolved checkmark icon to be green when all discussions resolved. +- [FIXED] Fix 500 error on merged merge requests when GitLab is restored from a backup. + ## 9.5.5 (2017-09-18) - [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 4b9fcbec101..a918a2aa18d 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.5.1 +0.6.0 @@ -105,7 +105,7 @@ gem 'fog-rackspace', '~> 0.1.1' gem 'fog-aliyun', '~> 0.1.0' # for Google storage -gem 'google-api-client', '~> 0.8.6' +gem 'google-api-client', '~> 0.13.6' # for aws storage gem 'unf', '~> 0.1.4' @@ -239,7 +239,7 @@ gem 'rack-proxy', '~> 0.6.0' gem 'sass-rails', '~> 5.0.6' gem 'uglifier', '~> 2.7.2' -gem 'addressable', '~> 2.3.8' +gem 'addressable', '~> 2.5.2' gem 'bootstrap-sass', '~> 3.3.0' gem 'font-awesome-rails', '~> 4.7' gem 'gemojione', '~> 3.3' @@ -356,7 +356,7 @@ end group :test do gem 'shoulda-matchers', '~> 3.1.2', require: false gem 'email_spec', '~> 1.6.0' - gem 'json-schema', '~> 2.6.2' + gem 'json-schema', '~> 2.8.0' gem 'webmock', '~> 2.3.2' gem 'test_after_commit', '~> 1.1' gem 'sham_rack', '~> 1.3.6' @@ -398,7 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.38.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 03ffb880fc9..a0ad2716c01 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -45,7 +45,8 @@ GEM adamantium (0.2.0) ice_nine (~> 0.11.0) memoizable (~> 0.4.0) - addressable (2.3.8) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) akismet (2.0.0) allocations (1.0.5) arel (6.0.4) @@ -62,10 +63,6 @@ GEM attr_encrypted (3.0.3) encryptor (~> 3.0.0) attr_required (1.0.0) - autoparse (0.3.3) - addressable (>= 2.3.1) - extlib (>= 0.9.15) - multi_json (>= 1.0.0) autoprefixer-rails (6.2.3) execjs json @@ -146,6 +143,8 @@ GEM debugger-ruby_core_source (1.3.8) deckar01-task_list (2.0.0) html-pipeline + declarative (0.0.10) + declarative-option (0.1.0) default_value_for (3.0.2) activerecord (>= 3.2.0, < 5.1) descendants_tracker (0.0.4) @@ -188,7 +187,6 @@ GEM excon (0.57.1) execjs (2.6.0) expression_parser (0.9.0) - extlib (0.9.16) factory_girl (4.7.0) activesupport (>= 3.0.0) factory_girl_rails (4.7.0) @@ -275,7 +273,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.38.0) + gitaly-proto (0.39.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -288,10 +286,10 @@ GEM flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json - gitlab-grit (2.8.1) + gitlab-grit (2.8.2) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) - mime-types (>= 1.16, < 3) + mime-types (>= 1.16) posix-spawn (~> 0.3) gitlab-markup (1.6.2) gitlab_omniauth-ldap (2.0.4) @@ -319,20 +317,16 @@ GEM json multi_json request_store (>= 1.0) - google-api-client (0.8.7) - activesupport (>= 3.2, < 5.0) - addressable (~> 2.3) - autoparse (~> 0.3) - extlib (~> 0.9) - faraday (~> 0.9) - googleauth (~> 0.3) - launchy (~> 2.4) - multi_json (~> 1.10) - retriable (~> 1.4) - signet (~> 0.6) + google-api-client (0.13.6) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.5) + httpclient (>= 2.8.1, < 3.0) + mime-types (~> 3.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) google-protobuf (3.4.0.2) - googleauth (0.5.1) - faraday (~> 0.9) + googleauth (0.5.3) + faraday (~> 0.12) jwt (~> 1.4) logging (~> 2.0) memoist (~> 0.12) @@ -422,8 +416,8 @@ GEM multi_json (>= 1.3) securecompare url_safe_base64 - json-schema (2.6.2) - addressable (~> 2.3.8) + json-schema (2.8.0) + addressable (>= 2.4) jwt (1.5.6) kaminari (1.0.1) activesupport (>= 4.1.0) @@ -475,18 +469,20 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) mail_room (0.9.1) - memoist (0.15.0) + memoist (0.16.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) method_source (0.8.2) - mime-types (2.99.3) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mimemagic (0.3.0) mini_mime (0.1.4) mini_portile2 (2.3.0) minitest (5.7.0) mmap2 (2.2.7) mousetrap-rails (1.4.6) - multi_json (1.12.1) + multi_json (1.12.2) multi_xml (0.6.0) multipart-post (2.0.0) mustermann (1.0.0) @@ -635,6 +631,7 @@ GEM pry (~> 0.10) pry-rails (0.3.5) pry (>= 0.9.10) + public_suffix (3.0.0) pyu-ruby-sasl (0.0.3.3) rack (1.6.8) rack-accept (0.4.5) @@ -717,6 +714,10 @@ GEM redis-store (~> 1.2.0) redis-store (1.2.0) redis (>= 2.2) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) request_store (1.3.1) responders (2.3.0) railties (>= 4.2.0, < 5.1) @@ -724,7 +725,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - retriable (1.4.1) + retriable (3.1.1) rinku (2.0.0) rotp (2.1.2) rouge (2.2.1) @@ -903,6 +904,7 @@ GEM tzinfo (1.2.3) thread_safe (~> 0.1) u2f (0.2.1) + uber (0.1.0) uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) @@ -963,7 +965,7 @@ DEPENDENCIES ace-rails-ap (~> 4.1.0) activerecord_sane_schema_dumper (= 0.2) acts-as-taggable-on (~> 4.0) - addressable (~> 2.3.8) + addressable (~> 2.5.2) akismet (~> 2.0) allocations (~> 1.0) asana (~> 0.6.0) @@ -1025,7 +1027,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.38.0) + gitaly-proto (~> 0.39.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1033,7 +1035,7 @@ DEPENDENCIES gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) - google-api-client (~> 0.8.6) + google-api-client (~> 0.13.6) gpgme grape (~> 1.0) grape-entity (~> 0.6.0) @@ -1051,7 +1053,7 @@ DEPENDENCIES jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - json-schema (~> 2.6.2) + json-schema (~> 2.8.0) jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.11.0) diff --git a/PROCESS.md b/PROCESS.md index ed4e84dd0b6..5e65bb59246 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either the version currently running on GitLab.com, or the most recent version available in the package repositories. +A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc) +and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc), +just like any other issue, to help GitLab team members focus on issues that are +relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on). + ## Release retrospective and kickoff - [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective) diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index e3e2c798570..93b0cbf4209 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -298,7 +298,7 @@ class CopyAsGFM { const documentFragment = getSelectedFragment(); if (!documentFragment) return; - const el = transformer(documentFragment.cloneNode(true)); + const el = transformer(documentFragment.cloneNode(true), e.currentTarget); if (!el) return; e.preventDefault(); @@ -338,55 +338,64 @@ class CopyAsGFM { } static transformGFMSelection(documentFragment) { - const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); - switch (gfmEls.length) { + const gfmElements = documentFragment.querySelectorAll('.md, .wiki'); + switch (gfmElements.length) { case 0: { return documentFragment; } case 1: { - return gfmEls[0]; + return gfmElements[0]; } default: { - const allGfmEl = document.createElement('div'); + const allGfmElement = document.createElement('div'); - for (let i = 0; i < gfmEls.length; i += 1) { - const lineEl = gfmEls[i]; - allGfmEl.appendChild(lineEl); - allGfmEl.appendChild(document.createTextNode('\n\n')); + for (let i = 0; i < gfmElements.length; i += 1) { + const gfmElement = gfmElements[i]; + allGfmElement.appendChild(gfmElement); + allGfmElement.appendChild(document.createTextNode('\n\n')); } - return allGfmEl; + return allGfmElement; } } } - static transformCodeSelection(documentFragment) { - const lineEls = documentFragment.querySelectorAll('.line'); + static transformCodeSelection(documentFragment, target) { + let lineSelector = '.line'; - let codeEl; - if (lineEls.length > 1) { - codeEl = document.createElement('pre'); - codeEl.className = 'code highlight'; + if (target) { + const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0]; + if (lineClass) { + lineSelector = `.line_content.${lineClass} ${lineSelector}`; + } + } + + const lineElements = documentFragment.querySelectorAll(lineSelector); + + let codeElement; + if (lineElements.length > 1) { + codeElement = document.createElement('pre'); + codeElement.className = 'code highlight'; - const lang = lineEls[0].getAttribute('lang'); + const lang = lineElements[0].getAttribute('lang'); if (lang) { - codeEl.setAttribute('lang', lang); + codeElement.setAttribute('lang', lang); } } else { - codeEl = document.createElement('code'); + codeElement = document.createElement('code'); } - if (lineEls.length > 0) { - for (let i = 0; i < lineEls.length; i += 1) { - const lineEl = lineEls[i]; - codeEl.appendChild(lineEl); - codeEl.appendChild(document.createTextNode('\n')); + if (lineElements.length > 0) { + for (let i = 0; i < lineElements.length; i += 1) { + const lineElement = lineElements[i]; + codeElement.appendChild(lineElement); + codeElement.appendChild(document.createTextNode('\n')); } } else { - codeEl.appendChild(documentFragment); + codeElement.appendChild(documentFragment); } - return codeEl; + return codeElement; } static nodeToGFM(node, respectWhitespaceParam = false) { diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 6a008112203..ae8338f5fd2 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -24,7 +24,8 @@ class Diff { if (!isBound) { $(document) .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) - .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)) + .on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this)); isBound = true; } @@ -100,6 +101,18 @@ class Diff { this.highlightSelectedLine(); } + handleParallelLineDown(e) { + const line = $(e.currentTarget); + const table = line.closest('table'); + + table.removeClass('left-side-selected right-side-selected'); + + const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0]; + if (lineClass) { + table.addClass(`${lineClass}-selected`); + } + } + diffViewType() { return $('.inline-parallel-buttons a.active').data('view-type'); } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 50d822eba5a..ff218ccad62 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -548,6 +548,7 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.positionMenuAbove = function() { var $menu = this.dropdown.find('.dropdown-menu'); + $menu.addClass('dropdown-open-top'); $menu.css('top', 'initial'); $menu.css('bottom', '100%'); }; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 6a5084efeb8..ce05b3eabec 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,28 +1,12 @@ import Jed from 'jed'; - -/** - This is required to require all the translation folders in the current directory - this saves us having to do this manually & keep up to date with new languages -**/ -function requireAll(requireContext) { return requireContext.keys().map(requireContext); } - -const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/)); -const locales = allLocales.reduce((d, obj) => { - const data = d; - const localeKey = Object.keys(obj)[0]; - - data[localeKey] = obj[localeKey]; - - return data; -}, {}); +import sprintf from './sprintf'; const langAttribute = document.querySelector('html').getAttribute('lang'); const lang = (langAttribute || 'en').replace(/-/g, '_'); -const locale = new Jed(locales[lang]); +const locale = new Jed(window.translations || {}); /** Translates `text` - @param text The text to be translated @returns {String} The translated text **/ @@ -66,4 +50,5 @@ export { lang }; export { gettext as __ }; export { ngettext as n__ }; export { pgettext as s__ }; +export { sprintf }; export default locale; diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js new file mode 100644 index 00000000000..5f4a053f98e --- /dev/null +++ b/app/assets/javascripts/locale/sprintf.js @@ -0,0 +1,26 @@ +import _ from 'underscore'; + +/** + Very limited implementation of sprintf supporting only named parameters. + + @param input (translated) text with parameters (e.g. '%{num_users} users use us') + @param parameters object mapping parameter names to values (e.g. { num_users: 5 }) + @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) + @returns {String} the text with parameters replaces (e.g. '5 users use us') + + @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf + @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 +**/ +export default (input, parameters, escapeParameters = true) => { + let output = input; + + if (parameters) { + Object.keys(parameters).forEach((parameterName) => { + const parameterValue = parameters[parameterName]; + const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue; + output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue); + }); + } + + return output; +}; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 0db2abe507d..af0658eb668 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper'; $el.text(gl.text.addDelimiter(count)); }; + MergeRequest.prototype.hideCloseButton = function() { + const el = document.querySelector('.merge-request .issuable-actions'); + const closeDropdownItem = el.querySelector('li.close-item'); + if (closeDropdownItem) { + closeDropdownItem.classList.add('hidden'); + // Selects the next dropdown item + el.querySelector('li.report-item').click(); + } else { + // No dropdown just hide the Close button + el.querySelector('.btn-close').classList.add('hidden'); + } + // Dropdown for mobile screen + el.querySelector('li.js-close-item').classList.add('hidden'); + }; + return MergeRequest; })(); }).call(window); diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index b8a16356576..b4067d229aa 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -1,18 +1,3 @@ -<template> - <div class="cell"> - <code-cell - type="input" - :raw-code="rawInputCode" - :count="cell.execution_count" - :code-css-class="codeCssClass" /> - <output-cell - v-if="hasOutput" - :count="cell.execution_count" - :output="output" - :code-css-class="codeCssClass" /> - </div> -</template> - <script> import CodeCell from './code/index.vue'; import OutputCell from './output/index.vue'; @@ -51,6 +36,21 @@ export default { }; </script> +<template> + <div class="cell"> + <code-cell + type="input" + :raw-code="rawInputCode" + :count="cell.execution_count" + :code-css-class="codeCssClass" /> + <output-cell + v-if="hasOutput" + :count="cell.execution_count" + :output="output" + :code-css-class="codeCssClass" /> + </div> +</template> + <style scoped> .cell { flex-direction: column; diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 31b30f601e2..0f3083f05b2 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -1,17 +1,3 @@ -<template> - <div :class="type"> - <prompt - :type="promptType" - :count="count" /> - <pre - class="language-python" - :class="codeCssClass" - ref="code" - v-text="code"> - </pre> - </div> -</template> - <script> import Prism from '../../lib/highlight'; import Prompt from '../prompt.vue'; @@ -55,3 +41,17 @@ }, }; </script> + +<template> + <div :class="type"> + <prompt + :type="promptType" + :count="count" /> + <pre + class="language-python" + :class="codeCssClass" + ref="code" + v-text="code"> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 814d2ea92b4..82c51a1068c 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,10 +1,3 @@ -<template> - <div class="cell text-cell"> - <prompt /> - <div class="markdown" v-html="markdown"></div> - </div> -</template> - <script> /* global katex */ import marked from 'marked'; @@ -95,6 +88,13 @@ }; </script> +<template> + <div class="cell text-cell"> + <prompt /> + <div class="markdown" v-html="markdown"></div> + </div> +</template> + <style> .markdown .katex { display: block; diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 0f39cd138df..2110a9de7ed 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,10 +1,3 @@ -<template> - <div class="output"> - <prompt /> - <div v-html="rawCode"></div> - </div> -</template> - <script> import Prompt from '../prompt.vue'; @@ -20,3 +13,10 @@ export default { }, }; </script> + +<template> + <div class="output"> + <prompt /> + <div v-html="rawCode"></div> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index f3b873bbc0f..fbb39ea6e2d 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,11 +1,3 @@ -<template> - <div class="output"> - <prompt /> - <img - :src="'data:' + outputType + ';base64,' + rawCode" /> - </div> -</template> - <script> import Prompt from '../prompt.vue'; @@ -25,3 +17,11 @@ export default { }, }; </script> + +<template> + <div class="output"> + <prompt /> + <img + :src="'data:' + outputType + ';base64,' + rawCode" /> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 23c9ea78939..05af0bf1e8e 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,12 +1,3 @@ -<template> - <component :is="componentName" - type="output" - :outputType="outputType" - :count="count" - :raw-code="rawCode" - :code-css-class="codeCssClass" /> -</template> - <script> import CodeCell from '../code/index.vue'; import Html from './html.vue'; @@ -81,3 +72,12 @@ export default { }, }; </script> + +<template> + <component :is="componentName" + type="output" + :outputType="outputType" + :count="count" + :raw-code="rawCode" + :code-css-class="codeCssClass" /> +</template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 4540e4248d8..039fb99293d 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -1,11 +1,3 @@ -<template> - <div class="prompt"> - <span v-if="type && count"> - {{ type }} [{{ count }}]: - </span> - </div> -</template> - <script> export default { props: { @@ -21,6 +13,14 @@ }; </script> +<template> + <div class="prompt"> + <span v-if="type && count"> + {{ type }} [{{ count }}]: + </span> + </div> +</template> + <style scoped> .prompt { padding: 0 10px; diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index fd62c1231ef..e88806431af 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -1,14 +1,3 @@ -<template> - <div v-if="hasNotebook"> - <component - v-for="(cell, index) in cells" - :is="cellType(cell.cell_type)" - :cell="cell" - :key="index" - :code-css-class="codeCssClass" /> - </div> -</template> - <script> import { MarkdownCell, @@ -59,6 +48,17 @@ }; </script> +<template> + <div v-if="hasNotebook"> + <component + v-for="(cell, index) in cells" + :is="cellType(cell.cell_type)" + :cell="cell" + :key="index" + :code-css-class="codeCssClass" /> + </div> +</template> + <style> .cell, .input, diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index fa7ac994058..1a7da84a424 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -272,6 +272,7 @@ v-model="note" ref="textarea" slot="textarea" + :disabled="isSubmitting" placeholder="Write a comment or drag your files here..." @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()"> diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index b874e484d45..c8a2f778ee8 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -1,13 +1,3 @@ -<template> - <div class="pdf-viewer" v-if="hasPDF"> - <page v-for="(page, index) in pages" - :key="index" - :v-if="!loading" - :page="page" - :number="index + 1" /> - </div> -</template> - <script> import pdfjsLib from 'vendor/pdf'; import workerSrc from 'vendor/pdf.worker.min'; @@ -64,6 +54,16 @@ }; </script> +<template> + <div class="pdf-viewer" v-if="hasPDF"> + <page v-for="(page, index) in pages" + :key="index" + :v-if="!loading" + :page="page" + :number="index + 1" /> + </div> +</template> + <style> .pdf-viewer { background: url('./assets/img/bg.gif'); diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index 7b74ee4eb2e..be38f7cc129 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -1,10 +1,3 @@ -<template> - <canvas - class="pdf-page" - ref="canvas" - :data-page="number" /> -</template> - <script> export default { props: { @@ -48,6 +41,13 @@ }; </script> +<template> + <canvas + class="pdf-page" + ref="canvas" + :data-page="number" /> +</template> + <style> .pdf-page { margin: 8px auto 0 auto; diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue new file mode 100644 index 00000000000..2d8ca443ea7 --- /dev/null +++ b/app/assets/javascripts/registry/components/app.vue @@ -0,0 +1,62 @@ +<script> + /* globals Flash */ + import { mapGetters, mapActions } from 'vuex'; + import '../../flash'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import store from '../stores'; + import collapsibleContainer from './collapsible_container.vue'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + name: 'registryListApp', + props: { + endpoint: { + type: String, + required: true, + }, + }, + store, + components: { + collapsibleContainer, + loadingIcon, + }, + computed: { + ...mapGetters([ + 'isLoading', + 'repos', + ]), + }, + methods: { + ...mapActions([ + 'setMainEndpoint', + 'fetchRepos', + ]), + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos() + .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); + }, + }; +</script> +<template> + <div> + <loading-icon + v-if="isLoading" + size="3" + /> + + <collapsible-container + v-else-if="!isLoading && repos.length" + v-for="(item, index) in repos" + :key="index" + :repo="item" + /> + + <p v-else-if="!isLoading && !repos.length"> + {{__("No container images stored for this project. Add one by following the instructions above.")}} + </p> + </div> +</template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue new file mode 100644 index 00000000000..41ea9742406 --- /dev/null +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -0,0 +1,131 @@ +<script> + /* globals Flash */ + import { mapActions } from 'vuex'; + import '../../flash'; + import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import tableRegistry from './table_registry.vue'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + name: 'collapsibeContainerRegisty', + props: { + repo: { + type: Object, + required: true, + }, + }, + components: { + clipboardButton, + loadingIcon, + tableRegistry, + }, + directives: { + tooltip, + }, + data() { + return { + isOpen: false, + }; + }, + computed: { + clipboardText() { + return `docker pull ${this.repo.location}`; + }, + }, + methods: { + ...mapActions([ + 'fetchRepos', + 'fetchList', + 'deleteRepo', + ]), + + toggleRepo() { + this.isOpen = !this.isOpen; + + if (this.isOpen) { + this.fetchList({ repo: this.repo }) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); + } + }, + + handleDeleteRepository() { + this.deleteRepo(this.repo) + .then(() => this.fetchRepos()) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, + + showError(message) { + Flash((errorMessages[message])); + }, + }, + }; +</script> + +<template> + <div class="container-image"> + <div + class="container-image-head"> + <button + type="button" + @click="toggleRepo" + class="js-toggle-repo btn-link"> + <i + class="fa" + :class="{ + 'fa-chevron-right': !isOpen, + 'fa-chevron-up': isOpen, + }" + aria-hidden="true"> + </i> + {{repo.name}} + </button> + + <clipboard-button + v-if="repo.location" + :text="clipboardText" + :title="repo.location" + /> + + <div class="controls hidden-xs pull-right"> + <button + v-if="repo.canDelete" + type="button" + class="js-remove-repo btn btn-danger" + :title="s__('ContainerRegistry|Remove repository')" + :aria-label="s__('ContainerRegistry|Remove repository')" + v-tooltip + @click="handleDeleteRepository"> + <i + class="fa fa-trash" + aria-hidden="true"> + </i> + </button> + </div> + + </div> + + <loading-icon + v-if="repo.isLoading" + class="append-bottom-20" + size="2" + /> + + <div + v-else-if="!repo.isLoading && isOpen" + class="container-image-tags"> + + <table-registry + v-if="repo.list.length" + :repo="repo" + /> + + <div + v-else + class="nothing-here-block"> + {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue new file mode 100644 index 00000000000..4ce1571b0aa --- /dev/null +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -0,0 +1,137 @@ +<script> + /* globals Flash */ + import { mapActions } from 'vuex'; + import { n__ } from '../../locale'; + import '../../flash'; + import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; + import tablePagination from '../../vue_shared/components/table_pagination.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import timeagoMixin from '../../vue_shared/mixins/timeago'; + import { errorMessages, errorMessagesTypes } from '../constants'; + + export default { + props: { + repo: { + type: Object, + required: true, + }, + }, + components: { + clipboardButton, + tablePagination, + }, + mixins: [ + timeagoMixin, + ], + directives: { + tooltip, + }, + computed: { + shouldRenderPagination() { + return this.repo.pagination.total > this.repo.pagination.perPage; + }, + }, + methods: { + ...mapActions([ + 'fetchList', + 'deleteRegistry', + ]), + + layers(item) { + return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; + }, + + handleDeleteRegistry(registry) { + this.deleteRegistry(registry) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + }, + + onPageChange(pageNumber) { + this.fetchList({ repo: this.repo, page: pageNumber }) + .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); + }, + + clipboardText(text) { + return `docker pull ${text}`; + }, + + showError(message) { + Flash((errorMessages[message])); + }, + }, + }; +</script> +<template> +<div> + <table class="table tags"> + <thead> + <tr> + <th>{{s__('ContainerRegistry|Tag')}}</th> + <th>{{s__('ContainerRegistry|Tag ID')}}</th> + <th>{{s__("ContainerRegistry|Size")}}</th> + <th>{{s__("ContainerRegistry|Created")}}</th> + <th></th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, i) in repo.list" + :key="i"> + <td> + + {{item.tag}} + + <clipboard-button + v-if="item.location" + :title="item.location" + :text="clipboardText(item.location)" + /> + </td> + <td> + <span + v-tooltip + :title="item.revision" + data-placement="bottom"> + {{item.shortRevision}} + </span> + </td> + <td> + {{item.size}} + <template v-if="item.size && item.layers"> + · + </template> + {{layers(item)}} + </td> + + <td> + {{timeFormated(item.createdAt)}} + </td> + + <td class="content"> + <button + v-if="item.canDelete" + type="button" + class="js-delete-registry btn btn-danger hidden-xs pull-right" + :title="s__('ContainerRegistry|Remove tag')" + :aria-label="s__('ContainerRegistry|Remove tag')" + data-container="body" + v-tooltip + @click="handleDeleteRegistry(item)"> + <i + class="fa fa-trash" + aria-hidden="true"> + </i> + </button> + </td> + </tr> + </tbody> + </table> + + <table-pagination + v-if="shouldRenderPagination" + :change="onPageChange" + :page-info="repo.pagination" + /> +</div> +</template> diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js new file mode 100644 index 00000000000..712b0fade3d --- /dev/null +++ b/app/assets/javascripts/registry/constants.js @@ -0,0 +1,15 @@ +import { __ } from '../locale'; + +export const errorMessagesTypes = { + FETCH_REGISTRY: 'FETCH_REGISTRY', + FETCH_REPOS: 'FETCH_REPOS', + DELETE_REPO: 'DELETE_REPO', + DELETE_REGISTRY: 'DELETE_REGISTRY', +}; + +export const errorMessages = { + [errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'), + [errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'), + [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'), + [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'), +}; diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js new file mode 100644 index 00000000000..d8edff73f72 --- /dev/null +++ b/app/assets/javascripts/registry/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import registryApp from './components/app.vue'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#js-vue-registry-images', + components: { + registryApp, + }, + data() { + const dataset = document.querySelector(this.$options.el).dataset; + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('registry-app', { + props: { + endpoint: this.endpoint, + }, + }); + }, +})); diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js new file mode 100644 index 00000000000..34ed40b8b65 --- /dev/null +++ b/app/assets/javascripts/registry/stores/actions.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import * as types from './mutation_types'; + +Vue.use(VueResource); + +export const fetchRepos = ({ commit, state }) => { + commit(types.TOGGLE_MAIN_LOADING); + + return Vue.http.get(state.endpoint) + .then(res => res.json()) + .then((response) => { + commit(types.TOGGLE_MAIN_LOADING); + commit(types.SET_REPOS_LIST, response); + }); +}; + +export const fetchList = ({ commit }, { repo, page }) => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + + return Vue.http.get(repo.tagsPath, { params: { page } }) + .then((response) => { + const headers = response.headers; + + return response.json().then((resp) => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); + }); + }); +}; + +export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath) + .then(res => res.json()); + +export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath) + .then(res => res.json()); + +export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); +export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js new file mode 100644 index 00000000000..588f479c492 --- /dev/null +++ b/app/assets/javascripts/registry/stores/getters.js @@ -0,0 +1,2 @@ +export const isLoading = state => state.isLoading; +export const repos = state => state.repos; diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js new file mode 100644 index 00000000000..78b67881210 --- /dev/null +++ b/app/assets/javascripts/registry/stores/index.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: { + isLoading: false, + endpoint: '', // initial endpoint to fetch the repos list + /** + * Each object in `repos` has the following strucure: + * { + * name: String, + * isLoading: Boolean, + * tagsPath: String // endpoint to request the list + * destroyPath: String // endpoit to delete the repo + * list: Array // List of the registry images + * } + * + * Each registry image inside `list` has the following structure: + * { + * tag: String, + * revision: String + * shortRevision: String + * size: Number + * layers: Number + * createdAt: String + * destroyPath: String // endpoit to delete each image + * } + */ + repos: [], + }, + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js new file mode 100644 index 00000000000..2c69bf11807 --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -0,0 +1,7 @@ +export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT'; + +export const SET_REPOS_LIST = 'SET_REPOS_LIST'; +export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING'; + +export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST'; +export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING'; diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js new file mode 100644 index 00000000000..e40382e7afc --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -0,0 +1,54 @@ +import * as types from './mutation_types'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; + +export default { + + [types.SET_MAIN_ENDPOINT](state, endpoint) { + Object.assign(state, { endpoint }); + }, + + [types.SET_REPOS_LIST](state, list) { + Object.assign(state, { + repos: list.map(el => ({ + canDelete: !!el.destroy_path, + destroyPath: el.destroy_path, + id: el.id, + isLoading: false, + list: [], + location: el.location, + name: el.path, + tagsPath: el.tags_path, + })), + }); + }, + + [types.TOGGLE_MAIN_LOADING](state) { + Object.assign(state, { isLoading: !state.isLoading }); + }, + + [types.SET_REGISTRY_LIST](state, { repo, resp, headers }) { + const listToUpdate = state.repos.find(el => el.id === repo.id); + + const normalizedHeaders = normalizeHeaders(headers); + const pagination = parseIntPagination(normalizedHeaders); + + listToUpdate.pagination = pagination; + + listToUpdate.list = resp.map(element => ({ + tag: element.name, + revision: element.revision, + shortRevision: element.short_revision, + size: element.size, + layers: element.layers, + location: element.location, + createdAt: element.created_at, + destroyPath: element.destroy_path, + canDelete: !!element.destroy_path, + })); + }, + + [types.TOGGLE_REGISTRY_LIST_LOADING](state, list) { + const listToUpdate = state.repos.find(el => el.id === list.id); + listToUpdate.isLoading = !listToUpdate.isLoading; + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js index 703f3a56a34..4998a47b691 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js @@ -27,7 +27,7 @@ export default { <button v-if="showDisabledButton" type="button" - class="btn btn-success btn-sm" + class="js-disabled-merge-button btn btn-success btn-sm" disabled="true"> Merge </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js index aaf9d3304a4..09561694939 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="loading" showDisabledButton /> + <status-icon status="loading" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Checking ability to merge automatically diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js index 4078aad7f83..b25cc3443ef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js @@ -16,9 +16,9 @@ export default { <div class="media-body"> <mr-widget-author-and-time actionText="Closed by" - :author="mr.closedBy" - :dateTitle="mr.updatedAt" - :dateReadable="mr.closedAt" + :author="mr.closedEvent.author" + :dateTitle="mr.closedEvent.updatedAt" + :dateReadable="mr.closedEvent.formattedUpdatedAt" /> <section class="mr-info-list"> <p> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js index f9cb79a0bc1..5d468a085cb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js @@ -10,27 +10,37 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon + status="failed" + :show-disabled-button="true" /> <div class="media-body space-children"> - <span class="bold"> - There are merge conflicts<span v-if="!mr.canMerge">.</span> - <span v-if="!mr.canMerge"> - Resolve these conflicts or ask someone with write access to this repository to merge it locally - </span> + <span + v-if="mr.shouldBeRebased" + class="bold"> + Fast-forward merge is not possible. + To merge this request, first rebase locally. </span> - <a - v-if="mr.canMerge && mr.conflictResolutionPath" - :href="mr.conflictResolutionPath" - class="btn btn-default btn-xs js-resolve-conflicts-button"> - Resolve conflicts - </a> - <a - v-if="mr.canMerge" - class="btn btn-default btn-xs js-merge-locally-button" - data-toggle="modal" - href="#modal_merge_info"> - Merge locally - </a> + <template v-else> + <span class="bold"> + There are merge conflicts<span v-if="!mr.canMerge">.</span> + <span v-if="!mr.canMerge"> + Resolve these conflicts or ask someone with write access to this repository to merge it locally + </span> + </span> + <a + v-if="mr.canMerge && mr.conflictResolutionPath" + :href="mr.conflictResolutionPath" + class="js-resolve-conflicts-button btn btn-default btn-xs"> + Resolve conflicts + </a> + <a + v-if="mr.canMerge" + class="js-merge-locally-button btn btn-default btn-xs" + data-toggle="modal" + href="#modal_merge_info"> + Merge locally + </a> + </template> </div> </div> `, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js index 1cb24549d53..c25d6c359bb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js @@ -51,7 +51,7 @@ export default { </span> </template> <template v-else> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> <span diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js index e452260a4d0..74fc52796a0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js @@ -69,9 +69,9 @@ export default { <div class="space-children"> <mr-widget-author-and-time actionText="Merged by" - :author="mr.mergedBy" - :dateTitle="mr.updatedAt" - :dateReadable="mr.mergedAt" /> + :author="mr.mergedEvent.author" + :date-title="mr.mergedEvent.updatedAt" + :date-readable="mr.mergedEvent.formattedUpdatedAt" /> <a v-if="mr.canRevertInCurrentMR" v-tooltip diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js index 9f0a359d01a..1bc0b7e0819 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js @@ -24,7 +24,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold js-branch-text"> <span class="capitalize"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js index 797511d4e3a..00047718201 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="success" showDisabledButton /> + <status-icon status="success" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Ready to be merged automatically. diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js index 167a0d4613a..1cedf86e811 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Pipeline blocked. The pipeline for this merge request requires a manual action to proceed diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js index c5be9a0530a..6853ba4b9f8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index ad709da51ee..0c48a484fe8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -156,6 +156,7 @@ export default { eventHub.$emit('FetchActionsContent'); if (window.mergeRequest) { window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); + window.mergeRequest.hideCloseButton(); window.mergeRequest.decreaseCounter(); } stopPolling(); @@ -284,10 +285,16 @@ export default { :mr="mr" :is-merge-button-disabled="isMergeButtonDisabled" /> + <span + v-if="mr.ffOnlyEnabled" + class="js-fast-forward-message"> + Fast-forward merge without a merge commit + </span> <button + v-else @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" - class="btn btn-default btn-xs" + class="js-modify-commit-message-button btn btn-default btn-xs" type="button"> Modify commit message </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js index 89f38e5bd2a..af19cf6ab87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The source branch HEAD has recently changed. Please reload the page and review the changes before merging diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js index d762ca6e640..a119ecbbdfe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js @@ -10,7 +10,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> There are unresolved discussions. Please resolve these discussions diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index b11a06899cf..54be1fbe675 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -38,7 +38,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" /> + <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" /> <div class="media-body space-children"> <span class="bold"> This is a Work in Progress diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 29464662578..e554082149b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -37,10 +37,8 @@ export default class MergeRequestStore { } this.updatedAt = data.updated_at; - this.mergedAt = MergeRequestStore.getEventDate(data.merge_event); - this.closedAt = MergeRequestStore.getEventDate(data.closed_event); - this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event); - this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event); + this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event); + this.closedEvent = MergeRequestStore.getEventObject(data.closed_event); this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} }); this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; @@ -57,6 +55,8 @@ export default class MergeRequestStore { this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergePath = data.merge_path; + this.ffOnlyEnabled = data.ff_only_enabled; + this.shouldBeRebased = !!data.should_be_rebased; this.statusPath = data.status_path; this.emailPatchesPath = data.email_patches_path; this.plainDiffPath = data.plain_diff_path; @@ -118,6 +118,14 @@ export default class MergeRequestStore { } } + static getEventObject(event) { + return { + author: MergeRequestStore.getAuthorObject(event), + updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), + formattedUpdatedAt: MergeRequestStore.getEventDate(event), + }; + } + static getAuthorObject(event) { if (!event) { return {}; @@ -131,6 +139,14 @@ export default class MergeRequestStore { }; } + static getEventUpdatedAtDate(event) { + if (!event) { + return ''; + } + + return event.updated_at; + } + static getEventDate(event) { const timeagoInstance = new Timeago(); @@ -138,7 +154,7 @@ export default class MergeRequestStore { return ''; } - return timeagoInstance.format(event.updated_at); + return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event)); } } diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue new file mode 100644 index 00000000000..3a7143c450e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -0,0 +1,32 @@ +<script> + /** + * Falls back to the code used in `copy_to_clipboard.js` + */ + + export default { + name: 'clipboardButton', + props: { + text: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + }, + }; +</script> + +<template> + <button + type="button" + class="btn btn-transparent btn-clipboard" + :data-title="title" + :data-clipboard-text="text"> + <i + aria-hidden="true" + class="fa fa-clipboard"> + </i> + </button> +</template> diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index f83c4b00761..2c7886ec308 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -2,6 +2,7 @@ import { __, n__, s__, + sprintf, } from '../locale'; export default (Vue) => { @@ -37,6 +38,7 @@ export default (Vue) => { @returns {String} Translated context based text **/ s__, + sprintf, }, }); }; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index c0d8e6c328c..fa92d4ccf4f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -745,6 +745,10 @@ #{$selector}.dropdown-menu-nav { margin-bottom: 24px; + &.dropdown-open-top { + margin-bottom: $dropdown-vertical-offset; + } + li { display: block; padding: 0 1px; @@ -873,6 +877,13 @@ min-width: 100%; } } + + header.navbar-gitlab-new .header-content .dropdown { + .dropdown-menu { + left: 0; + min-width: 100%; + } + } } @include new-style-dropdown('.breadcrumbs-list .dropdown '); diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 0fb19344510..badc7b0eba3 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -229,6 +229,10 @@ ul.content-list { .label-default { color: $gl-text-color-secondary; } + + .avatar-cell { + align-self: flex-start; + } } .panel > .content-list > li { diff --git a/app/assets/stylesheets/framework/new-nav.scss b/app/assets/stylesheets/framework/new-nav.scss index 3abf3e4ac7d..7899be2c2d3 100644 --- a/app/assets/stylesheets/framework/new-nav.scss +++ b/app/assets/stylesheets/framework/new-nav.scss @@ -295,7 +295,7 @@ header.navbar-gitlab-new { .header-user .dropdown-menu-nav, .header-new .dropdown-menu-nav { - margin-top: 4px; + margin-top: $dropdown-vertical-offset; } .breadcrumbs { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e8bb42f4a8c..9bbda87dec9 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -327,6 +327,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; +$dropdown-vertical-offset: 4px; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, .04); diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index 3266714396e..dfff3e15556 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -9,6 +9,14 @@ .container-image-head { padding: 0 16px; line-height: 4em; + + .btn-link { + padding: 0; + + &:focus { + outline: none; + } + } } .table.tags { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e4bd783c8bc..fb23343b966 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -77,6 +77,18 @@ word-wrap: break-word; } } + + &.left-side-selected { + td.line_content.parallel.right-side { + @include user-select(none); + } + } + + &.right-side-selected { + td.line_content.parallel.left-side { + @include user-select(none); + } + } } tr.line_holder.parallel { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index be4db597689..74d9acb5490 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -362,7 +362,7 @@ .dropdown-menu { top: initial; - bottom: 40px; + bottom: 100%; width: 298px; } diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index fe22d186af1..a355e2dee24 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -12,3 +12,7 @@ margin-left: 10px; } } + +.registry-placeholder { + min-height: 60px; +} diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index b75e401a8df..db8c362f125 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -59,6 +59,7 @@ module AuthenticatesWithTwoFactor sign_in(user) else user.increment_failed_attempts! + Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP") flash.now[:alert] = 'Invalid two-factor code.' prompt_for_two_factor(user) end @@ -75,6 +76,7 @@ module AuthenticatesWithTwoFactor sign_in(user) else user.increment_failed_attempts! + Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F") flash.now[:alert] = 'Authentication via U2F device failed.' prompt_for_two_factor(user) end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 18fd8eb114d..915f32b4c33 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -15,9 +15,9 @@ module NotesActions notes = notes_finder.execute .inc_relations_for_view - .reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = prepare_notes_for_rendering(notes) + notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes_json[:notes] = if noteable.discussions_rendered_on_frontend? diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 10d2665c06a..0c2646d7bf0 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -14,6 +14,7 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(resource_name) after_sign_in(resource) else + Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") flash[:notice] += " Please sign in." new_session_path(resource_name) end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index c1cc509a748..4146deefa89 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,6 +1,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def index set_index_vars + @personal_access_token = finder.build end def create @@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def set_index_vars @scopes = Gitlab::Auth.available_scopes - @personal_access_token = finder.build @inactive_personal_access_tokens = finder(state: 'inactive').execute @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index a9cce578366..7f03ce07dec 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController def index @sort = params[:sort].presence || sort_value_recently_updated - @branches = BranchesFinder.new(@repository, params).execute + @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) respond_to do |format| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index a3ec79a56d9..ee6e6f80cdd 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:edit, :update, :move] + before_action :authorize_update_issue!, only: [:update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -63,10 +63,6 @@ class Projects::IssuesController < Projects::ApplicationController respond_with(@issue) end - def edit - respond_with(@issue) - end - def show @noteable = @issue @note = @project.notes.new(noteable: @issue) @@ -126,10 +122,6 @@ class Projects::IssuesController < Projects::ApplicationController @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) respond_to do |format| - format.html do - recaptcha_check_with_fallback { render :edit } - end - format.json do render_issue_json end diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 71e7dc70a4d..32c0fc6d14a 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -6,17 +6,26 @@ module Projects def index @images = project.container_repositories + + respond_to do |format| + format.html + format.json do + render json: ContainerRepositoriesSerializer + .new(project: project, current_user: current_user) + .represent(@images) + end + end end def destroy if image.destroy - redirect_to project_container_registry_index_path(@project), - status: 302, - notice: 'Image repository has been removed successfully!' + respond_to do |format| + format.json { head :no_content } + end else - redirect_to project_container_registry_index_path(@project), - status: 302, - alert: 'Failed to remove image repository!' + respond_to do |format| + format.json { head :bad_request } + end end end diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index ae72bd03cfb..e602aa3f393 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -3,20 +3,35 @@ module Projects class TagsController < ::Projects::Registry::ApplicationController before_action :authorize_update_container_image!, only: [:destroy] + def index + respond_to do |format| + format.json do + render json: ContainerTagsSerializer + .new(project: @project, current_user: @current_user) + .with_pagination(request, response) + .represent(tags) + end + end + end + def destroy if tag.delete - redirect_to project_container_registry_index_path(@project), - status: 302, - notice: 'Registry tag has been removed successfully!' + respond_to do |format| + format.json { head :no_content } + end else - redirect_to project_container_registry_index_path(@project), - status: 302, - alert: 'Failed to remove registry tag!' + respond_to do |format| + format.json { head :bad_request } + end end end private + def tags + Kaminari::PaginatableArray.new(image.tags, limit: 15) + end + def image @image ||= project.container_repositories .find(params[:repository_id]) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 968d880886c..a8ebdf5a4a9 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController response.headers['Content-Security-Policy'] = "default-src 'none'" response.headers['X-Content-Security-Policy'] = "default-src 'none'" - if file.on_disk? - send_file file.on_disk_path, disposition: 'inline' - else - send_data( - file.raw_data, - type: file.mime_type, - disposition: 'inline', - filename: file.name - ) - end + send_data( + file.raw_data, + type: file.mime_type, + disposition: 'inline', + filename: file.name + ) else return render('empty') unless can?(current_user, :create_wiki, @project) @page = WikiPage.new(@project_wiki) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b13034d3333..a738ca9f361 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -344,6 +344,7 @@ class ProjectsController < Projects::ApplicationController :tag_list, :visibility_level, :template_name, + :merge_method, project_feature_attributes: %i[ builds_access_level diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 1bc6520370a..5ea3a5d5562 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -42,10 +42,12 @@ class RegistrationsController < Devise::RegistrationsController end def after_sign_up_path_for(user) + Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}") user.confirmed? ? dashboard_projects_path : users_almost_there_path end - def after_inactive_sign_up_path_for(_resource) + def after_inactive_sign_up_path_for(resource) + Gitlab::AppLogger.info("User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:false") users_almost_there_path end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index fe3bb117410..4223c6171a6 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -13,6 +13,8 @@ class SessionsController < Devise::SessionsController before_action :auto_sign_in_with_provider, only: [:new] before_action :load_recaptcha + after_action :log_failed_login, only: [:new] + def new set_minimum_password_length @ldap_servers = Gitlab::LDAP::Config.available_servers @@ -29,12 +31,13 @@ class SessionsController < Devise::SessionsController end # hide the signed-in notification flash[:notice] = nil - log_audit_event(current_user, with: authentication_method) + log_audit_event(current_user, resource, with: authentication_method) log_user_activity(current_user) end end def destroy + Gitlab::AppLogger.info("User Logout: username=#{current_user.username} ip=#{request.remote_ip}") super # hide the signed_out notice flash[:notice] = nil @@ -42,6 +45,16 @@ class SessionsController < Devise::SessionsController private + def log_failed_login + return unless failed_login? + + Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") + end + + def failed_login? + (options = env["warden.options"]) && options[:action] == "unauthenticated" + end + def login_counter @login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count') end @@ -123,7 +136,8 @@ class SessionsController < Devise::SessionsController user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end - def log_audit_event(user, options = {}) + def log_audit_event(user, resource, options = {}) + Gitlab::AppLogger.info("Successful Login: username=#{resource.username} ip=#{request.remote_ip} method=#{options[:with]} admin=#{resource.admin?}") AuditEventService.new(user, user, options) .for_authentication.security_event end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 28f591a4e22..4e4a66e8a02 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -33,19 +33,21 @@ module DiffHelper end def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) - content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" - cls = ['diff-line-num', 'unfold', 'js-unfold'] - cls << 'js-unfold-bottom' if bottom + content_line_class = %w[line_content match] + content_line_class << 'parallel' if view == :parallel + + line_num_class = %w[diff-line-num unfold js-unfold] + line_num_class << 'js-unfold-bottom' if bottom html = '' if old_pos - html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) - html << content unless view == :inline + html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos }) + html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel end if new_pos - html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) - html << content + html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos }) + html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)]) end html.html_safe diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index b331693c789..fd88e0d794a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,13 +1,15 @@ module EventsHelper ICON_NAMES_BY_EVENT_TYPE = { - 'pushed to' => 'icon_commit', - 'pushed new' => 'icon_commit', - 'created' => 'icon_status_open', - 'opened' => 'icon_status_open', - 'closed' => 'icon_status_closed', - 'accepted' => 'icon_code_fork', - 'commented on' => 'icon_comment_o', - 'deleted' => 'icon_trash_o' + 'pushed to' => 'commit', + 'pushed new' => 'commit', + 'created' => 'status_open', + 'opened' => 'status_open', + 'closed' => 'status_closed', + 'accepted' => 'fork', + 'commented on' => 'comment', + 'deleted' => 'remove', + 'imported' => 'import', + 'joined' => 'users' }.freeze def link_to_author(event, self_added: false) @@ -197,7 +199,7 @@ module EventsHelper def icon_for_event(note) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - custom_icon(icon_name) if icon_name + sprite_icon(icon_name) if icon_name end def icon_for_profile_event(event) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ee544d8ac56..dd315866e60 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -229,6 +229,10 @@ module Ci variables end + def features + { trace_sections: true } + end + def merge_request return @merge_request if defined?(@merge_request) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 44deae4234b..54bd5b68777 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -73,7 +73,7 @@ class GpgKey < ActiveRecord::Base end def verified_and_belongs_to_email?(email) - emails_with_verified_status.fetch(email, false) + emails_with_verified_status.fetch(email.downcase, false) end def update_invalid_gpg_signatures diff --git a/app/models/key.rb b/app/models/key.rb index 0c41e34d969..f119b15c737 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -34,6 +34,7 @@ class Key < ActiveRecord::Base value&.delete!("\n\r") value.strip! unless value.blank? write_attribute(:key, value) + @public_key = nil end def publishable_key diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8d9a30397a9..0ba00d447e8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -524,6 +524,14 @@ class MergeRequest < ActiveRecord::Base true end + def ff_merge_possible? + project.repository.ancestor?(target_branch_sha, diff_head_sha) + end + + def should_be_rebased? + project.ff_merge_must_be_possible? && !ff_merge_possible? + end + def can_cancel_merge_when_pipeline_succeeds?(current_user) can_be_merged_by?(current_user) || self.author == current_user end @@ -552,14 +560,20 @@ class MergeRequest < ActiveRecord::Base commits_for_notes_limit = 100 commit_ids = commit_shas.take(commits_for_notes_limit) - Note.where( - "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + - "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", - mr_id: id, - commit_ids: commit_ids, - target_project_id: target_project_id, - source_project_id: source_project_id - ) + commit_notes = Note + .except(:order) + .where(project_id: [source_project_id, target_project_id]) + .where(noteable_type: 'Commit', commit_id: commit_ids) + + # We're using a UNION ALL here since this results in better performance + # compared to using OR statements. We're using UNION ALL since the queries + # used won't produce any duplicates (e.g. a note for a commit can't also be + # a note for an MR). + union = Gitlab::SQL::Union + .new([notes, commit_notes], remove_duplicates: false) + .to_sql + + Note.from("(#{union}) #{Note.table_name}") end alias_method :discussion_notes, :related_notes @@ -734,10 +748,9 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - has_ci_integration = source_project.try(:ci_service) - uses_gitlab_ci = all_pipelines.any? + return false if has_no_commits? - (has_ci_integration || uses_gitlab_ci) && commits.any? + !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service) end def branch_missing? diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 1f9d712ef84..cfcb03138b7 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base validates :scopes, presence: true validate :validate_scopes + after_initialize :set_default_scopes, if: :persisted? + def revoke! update!(revoked: true) end @@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base errors.add :scopes, "can only contain available scopes" end end + + def set_default_scopes + self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? + end end diff --git a/app/models/project.rb b/app/models/project.rb index bb3f74c4b89..59b5a5b3cd7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -64,6 +64,7 @@ class Project < ActiveRecord::Base # Storage specific hooks after_initialize :use_hashed_storage + after_create :check_repository_absence! after_create :ensure_storage_path_exists after_save :ensure_storage_path_exists, if: :namespace_id_changed? @@ -72,6 +73,7 @@ class Project < ActiveRecord::Base attr_accessor :old_path_with_namespace attr_accessor :template_name attr_writer :pipeline_status + attr_accessor :skip_disk_validation alias_attribute :title, :name @@ -227,7 +229,7 @@ class Project < ActiveRecord::Base validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create - validate :can_create_repository?, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? } + validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } validate :avatar_type, if: ->(project) { project.avatar.present? && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } @@ -1018,12 +1020,15 @@ class Project < ActiveRecord::Base end # Check if repository already exists on disk - def can_create_repository? + def check_repository_path_availability + return true if skip_disk_validation return false unless repository_storage_path expires_full_path_cache # we need to clear cache to validate renames correctly - if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") + # Check if repository with same path already exists on disk we can + # skip this for the hashed storage because the path does not change + if legacy_storage? && repository_with_same_path_already_exists? errors.add(:base, 'There is already a repository with that name on disk') return false end @@ -1564,6 +1569,34 @@ class Project < ActiveRecord::Base persisted? && path_changed? end + def merge_method + if self.merge_requests_ff_only_enabled + :ff + elsif self.merge_requests_rebase_enabled + :rebase_merge + else + :merge + end + end + + def merge_method=(method) + case method.to_s + when "ff" + self.merge_requests_ff_only_enabled = true + self.merge_requests_rebase_enabled = true + when "rebase_merge" + self.merge_requests_ff_only_enabled = false + self.merge_requests_rebase_enabled = true + when "merge" + self.merge_requests_ff_only_enabled = false + self.merge_requests_rebase_enabled = false + end + end + + def ff_merge_must_be_possible? + self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled + end + def migrate_to_hashed_storage! return if hashed_storage? @@ -1611,6 +1644,19 @@ class Project < ActiveRecord::Base Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value end + def check_repository_absence! + return if skip_disk_validation + + if repository_storage_path.blank? || repository_with_same_path_already_exists? + errors.add(:base, 'There is already a repository with that name on disk') + throw :abort + end + end + + def repository_with_same_path_already_exists? + gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") + end + # set last_activity_at to the same as created_at def set_last_activity_at update_column(:last_activity_at, self.created_at) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c4cc1c1cf22..bb7be29ef66 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -54,12 +54,15 @@ class ProjectWiki [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') end - # Returns the Gollum::Wiki object. + # Returns the Gitlab::Git::Wiki object. def wiki @wiki ||= begin - Gollum::Wiki.new(path_to_repo) - rescue Rugged::OSError - create_repo! + gl_repository = Gitlab::GlRepository.gl_repository(project, true) + raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository) + + create_repo!(raw_repository) unless raw_repository.exists? + + Gitlab::Git::Wiki.new(raw_repository) end end @@ -86,20 +89,14 @@ class ProjectWiki # Returns an initialized WikiPage instance or nil def find_page(title, version = nil) page_title, page_dir = page_title_and_dir(title) - if page = wiki.page(page_title, version, page_dir) + + if page = wiki.page(title: page_title, version: version, dir: page_dir) WikiPage.new(self, page, true) - else - nil end end - def find_file(name, version = nil, try_on_disk = true) - version = wiki.ref if version.nil? # Gollum::Wiki#file ? - if wiki_file = wiki.file(name, version, try_on_disk) - wiki_file - else - nil - end + def find_file(name, version = nil) + wiki.file(name, version) end def create_page(title, content, format = :markdown, message = nil) @@ -108,7 +105,7 @@ class ProjectWiki wiki.write_page(title, format.to_sym, content, commit) update_project_activity - rescue Gollum::DuplicatePageError => e + rescue Gitlab::Git::Wiki::DuplicatePageError => e @error_message = "Duplicate page: #{e.message}" return false end @@ -116,13 +113,13 @@ class ProjectWiki def update_page(page, content:, title: nil, format: :markdown, message: nil) commit = commit_details(:updated, message, page.title) - wiki.update_page(page, title || page.name, format.to_sym, content, commit) + wiki.update_page(page.path, title || page.name, format.to_sym, content, commit) update_project_activity end def delete_page(page, message = nil) - wiki.delete_page(page, commit_details(:deleted, message, page.title)) + wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) update_project_activity end @@ -145,20 +142,8 @@ class ProjectWiki wiki.class.default_ref end - def create_repo! - if init_repo(disk_path) - wiki = Gollum::Wiki.new(path_to_repo) - else - raise CouldNotCreateWikiError - end - - repository.after_create - - wiki - end - def ensure_repository - create_repo! unless repository_exists? + raise CouldNotCreateWikiError unless wiki.repository_exists? end def hook_attrs @@ -173,24 +158,24 @@ class ProjectWiki private - def init_repo(disk_path) + def create_repo!(raw_repository) gitlab_shell.add_repository(project.repository_storage, disk_path) + + raise CouldNotCreateWikiError unless raw_repository.exists? + + repository.after_create end def commit_details(action, message = nil, title = nil) commit_message = message || default_message(action, title) - { email: @user.email, name: @user.name, message: commit_message } + Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message) end def default_message(action, title) "#{@user.username} #{action} page: #{title}" end - def path_to_repo - @path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git") - end - def update_project_activity @project.touch(:last_activity_at, :last_repository_updated_at) end diff --git a/app/models/repository.rb b/app/models/repository.rb index a0f57f1e54d..d725c65081d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -850,6 +850,25 @@ class Repository end end + def ff_merge(user, source, target_branch, merge_request: nil) + our_commit = rugged.branches[target_branch].target + their_commit = + if source.is_a?(Gitlab::Git::Commit) + source.raw_commit + else + rugged.lookup(source) + end + + raise 'Invalid merge target' if our_commit.nil? + raise 'Invalid merge source' if their_commit.nil? + + with_branch(user, target_branch) do |start_commit| + merge_request&.update(in_progress_merge_commit_sha: their_commit.oid) + + their_commit.oid + end + end + def revert( user, commit, branch_name, message, start_branch_name: nil, start_project: project) @@ -970,7 +989,7 @@ class Repository end def create_ref(ref, ref_path) - fetch_ref(path_to_repo, ref, ref_path) + raw_repository.write_ref(ref_path, ref) end def ls_files(ref) diff --git a/app/models/user.rb b/app/models/user.rb index 4d523aa983f..4e71a3e11c2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -692,7 +692,11 @@ class User < ActiveRecord::Base end def ldap_user? - identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) + if identities.loaded? + identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? } + else + identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) + end end def ldap_identity @@ -1063,6 +1067,12 @@ class User < ActiveRecord::Base user_synced_attributes_metadata&.read_only?(attribute) end + # override, from Devise + def lock_access! + Gitlab::AppLogger.info("Account Locked: username=#{username}") + super + end + protected # override, from Devise::Validatable diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index f2315bb3dbb..5f710961f95 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -50,7 +50,7 @@ class WikiPage # The Gitlab ProjectWiki instance. attr_reader :wiki - # The raw Gollum::Page instance. + # The raw Gitlab::Git::WikiPage instance. attr_reader :page # The attributes Hash used for storing and validating @@ -75,7 +75,7 @@ class WikiPage if @attributes[:slug].present? @attributes[:slug] else - wiki.wiki.preview_page(title, '', format).url_path + wiki.wiki.preview_slug(title, format) end end @@ -131,7 +131,7 @@ class WikiPage def versions return [] unless persisted? - @page.versions + wiki.wiki.page_versions(@page.path) end def commit @@ -264,8 +264,8 @@ class WikiPage end page_title, page_dir = wiki.page_title_and_dir(page_details) - gollum_wiki = wiki.wiki - @page = gollum_wiki.paged(page_title, page_dir) + gitlab_git_wiki = wiki.wiki + @page = gitlab_git_wiki.page(title: page_title, dir: page_dir) set_attributes @persisted = errors.blank? diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 2df84e58575..a25882cbb62 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def remove_wip_path - if can?(current_user, :update_merge_request, merge_request.project) + if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project) remove_wip_project_merge_request_path(project, merge_request) end end diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb new file mode 100644 index 00000000000..56dc70b5687 --- /dev/null +++ b/app/serializers/container_repositories_serializer.rb @@ -0,0 +1,3 @@ +class ContainerRepositoriesSerializer < BaseSerializer + entity ContainerRepositoryEntity +end diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb new file mode 100644 index 00000000000..1103cf30a07 --- /dev/null +++ b/app/serializers/container_repository_entity.rb @@ -0,0 +1,25 @@ +class ContainerRepositoryEntity < Grape::Entity + include RequestAwareEntity + + expose :id, :path, :location + + expose :tags_path do |repository| + project_registry_repository_tags_path(project, repository, format: :json) + end + + expose :destroy_path, if: -> (*) { can_destroy? } do |repository| + project_container_registry_path(project, repository, format: :json) + end + + private + + alias_method :repository, :object + + def project + request.project + end + + def can_destroy? + can?(request.current_user, :update_container_image, project) + end +end diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb new file mode 100644 index 00000000000..ec1fc349586 --- /dev/null +++ b/app/serializers/container_tag_entity.rb @@ -0,0 +1,23 @@ +class ContainerTagEntity < Grape::Entity + include RequestAwareEntity + + expose :name, :location, :revision, :total_size, :created_at + + expose :destroy_path, if: -> (*) { can_destroy? } do |tag| + project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json) + end + + private + + alias_method :tag, :object + + def project + request.project + end + + def can_destroy? + # TODO: We check permission against @project, not tag, + # as tag is no AR object that is attached to project + can?(request.current_user, :update_container_image, project) + end +end diff --git a/app/serializers/container_tags_serializer.rb b/app/serializers/container_tags_serializer.rb new file mode 100644 index 00000000000..6ff3adff135 --- /dev/null +++ b/app/serializers/container_tags_serializer.rb @@ -0,0 +1,17 @@ +class ContainerTagsSerializer < BaseSerializer + entity ContainerTagEntity + + def with_pagination(request, response) + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } + end + + def paginated? + @paginator.present? + end + + def represent(resource, opts = {}) + resource = @paginator.paginate(resource) if paginated? + + super(resource, opts) + end +end diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 07650ce6f20..36537c5bd02 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -13,12 +13,16 @@ class MergeRequestEntity < IssuableEntity expose :target_branch expose :target_project_id + expose :should_be_rebased?, as: :should_be_rebased + expose :ff_only_enabled do |merge_request| + merge_request.project.merge_requests_ff_only_enabled + end + # Events expose :merge_event, using: EventEntity expose :closed_event, using: EventEntity # User entities - expose :author, using: UserEntity expose :merge_user, using: UserEntity # Diff sha's @@ -26,7 +30,6 @@ class MergeRequestEntity < IssuableEntity merge_request.diff_head_sha if merge_request.diff_head_commit end - expose :merge_commit_sha expose :merge_commit_message expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb new file mode 100644 index 00000000000..ba6853b835a --- /dev/null +++ b/app/services/merge_requests/ff_merge_service.rb @@ -0,0 +1,24 @@ +module MergeRequests + # MergeService class + # + # Do git fast-forward merge and in case of success + # mark merge request as merged and execute all hooks and notifications + # Executed when you do fast-forward merge via GitLab UI + # + class FfMergeService < MergeRequests::MergeService + private + + def commit + repository.ff_merge(current_user, + source, + merge_request.target_branch, + merge_request: merge_request) + rescue Gitlab::Git::HooksService::PreReceiveError => e + raise MergeError, e.message + rescue StandardError => e + raise MergeError, "Something went wrong during merge: #{e.message}" + ensure + merge_request.update(in_progress_merge_commit_sha: nil) + end + end +end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index bf26859dd6d..a110abf8256 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -11,6 +11,11 @@ module MergeRequests attr_reader :merge_request, :source def execute(merge_request) + if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService) + FfMergeService.new(project, current_user, params).execute(merge_request) + return + end + @merge_request = merge_request unless @merge_request.mergeable? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index e3a9e99250e..0d5350f873b 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -37,9 +37,9 @@ - if content_for?(:library_javascripts) = yield :library_javascripts + = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" - = webpack_bundle_tag "locale" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml new file mode 100644 index 00000000000..9d357293a2f --- /dev/null +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -0,0 +1,13 @@ +- form = local_assigns.fetch(:form) +- project = local_assigns.fetch(:project) + +.radio + = label_tag :project_merge_method_ff do + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio" + %strong Fast-forward merge + %br + %span.descr + No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. + %br + %span.descr + When fast-forward merge is not possible, the user must first rebase locally. diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml new file mode 100644 index 00000000000..c52e09573a6 --- /dev/null +++ b/app/views/projects/_merge_request_rebase_settings.html.haml @@ -0,0 +1,13 @@ +- form = local_assigns.fetch(:form) + +.radio + = label_tag :project_merge_method_rebase_merge do + = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" + %strong Merge commit with semi-linear history + %br + %span.descr + A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. + This way you could make sure that if this merge request would build, after merging to target branch it would also build. + %br + %span.descr + When fast-forward merge is not possible, the user must first rebase locally. diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index cc5afa943cf..fd0c419cdac 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,3 +1,18 @@ - form = local_assigns.fetch(:form) +.form-group + = label_tag :merge_method_merge, class: 'label-light' do + Merge method + .radio + = label_tag :project_merge_method_merge do + = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" + %strong Merge commit + %br + %span.descr + A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. + + = render 'merge_request_rebase_settings', form: form + + = render 'merge_request_fast_forward_settings', project: @project, form: form + = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index d1d448f0d4c..ea7a71792a3 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -5,25 +5,24 @@ = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view - @lines.each_with_index do |line, index| - - line_new = index + @form.since - - line_old = line_new - @form.offset - - line_content = capture do - %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} - %tr.line_holder.diff-expanded{ id: line_old, class: line_class } + - line_number_new = index + @form.since + - line_number_old = line_number_new - @form.offset + - line[0, 0] = ' ' * @form.indent + %tr.line_holder.diff-expanded{ id: line_number_old, class: line_class } - case diff_view - when :inline - %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{ href: "#", data: { linenumber: line_old }, disabled: true } - %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{ href: "#", data: { linenumber: line_new }, disabled: true } - = line_content + %td.old_line.diff-line-num{ data: { linenumber: line_number_old } } + %a{ href: "#", data: { linenumber: line_number_old }, disabled: true } + %td.new_line.diff-line-num{ data: { linenumber: line_number_new } } + %a{ href: "#", data: { linenumber: line_number_new }, disabled: true } + %td.line_content.noteable_line{ class: line_class }= line - when :parallel - %td.old_line.diff-line-num{ data: { linenumber: line_old } } - %a{ href: "##{line_old}", data: { linenumber: line_old }, disabled: true } - = line_content - %td.new_line.diff-line-num{ data: { linenumber: line_new } } - %a{ href: "##{line_new}", data: { linenumber: line_new }, disabled: true } - = line_content + %td.old_line.diff-line-num{ data: { linenumber: line_number_old } } + %a{ href: "##{line_number_old}", data: { linenumber: line_number_old }, disabled: true } + %td.line_content.noteable_line.left-side{ class: line_class }= line + %td.new_line.diff-line-num{ data: { linenumber: line_number_new } } + %a{ href: "##{line_number_new}", data: { linenumber: line_number_new }, disabled: true } + %td.line_content.noteable_line.right-side{ class: line_class }= line - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size %tr.line_holder{ id: @form.to, class: line_class } diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 56d63250714..1f0ca211074 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,20 +14,20 @@ = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num - %td.line_content.match= left.text + %td.line_content.match.left-side= left.text - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } } = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } - %td.line_content.parallel.noteable_line{ class: left.type }= diff_line_content(left.text) + %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell - %td.line_content.parallel + %td.line_content.parallel.left-side - if right - case right.type @@ -35,20 +35,20 @@ = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - when 'old-nonewline', 'new-nonewline' %td.new_line.diff-line-num - %td.line_content.match= right.text + %td.line_content.match.right-side= right.text - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } } = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } - %td.line_content.parallel.noteable_line{ class: right.type }= diff_line_content(right.text) + %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell - %td.line_content.parallel + %td.line_content.parallel.right-side - if discussions_left || discussions_right = render "discussions/parallel_diff_discussion", discussions_left: discussions_left, discussions_right: discussions_right diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 6a567487514..5f97d31f610 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,13 +2,13 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list.related-merge-requests - - has_any_ci = @merge_requests.any?(&:head_pipeline) + - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - if merge_request.head_pipeline = render_pipeline_status(merge_request.head_pipeline) - - elsif has_any_ci + - elsif has_any_head_pipeline = icon('blank fw') %span.merge-request-id = merge_request.to_reference diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml deleted file mode 100644 index 1b7d878c38c..00000000000 --- a/app/views/projects/issues/edit.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues" - -%h3.page-title - Edit Issue ##{@issue.iid} -%hr - -= render "form" diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index f3c44c94a5c..9ff85c2ee4c 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -29,7 +29,7 @@ - unless current_user == @merge_request.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request - %li{ class: merge_request_button_visibility(@merge_request, true) } + %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml deleted file mode 100644 index a0535edafc3..00000000000 --- a/app/views/projects/registry/repositories/_image.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.container-image.js-toggle-container - .container-image-head - = link_to "#", class: "js-toggle-button" do - = icon('chevron-down', 'aria-hidden': 'true') - = escape_once(image.path) - - = clipboard_button(clipboard_text: "docker pull #{image.location}") - - - if can?(current_user, :update_container_image, @project) - .controls.hidden-xs.pull-right - = link_to project_container_registry_path(@project, image), - class: 'btn btn-remove has-tooltip', - title: 'Remove repository', - data: { confirm: 'Are you sure?' }, - method: :delete do - = icon('trash cred', 'aria-hidden': 'true') - - .container-image-tags.js-toggle-content.hide - - if image.has_tags? - .table-holder - %table.table.tags - %thead - %tr - %th Tag - %th Tag ID - %th Size - %th Created - - if can?(current_user, :update_container_image, @project) - %th - = render partial: 'tag', collection: image.tags - - else - .nothing-here-block No tags in Container Registry for this container image. diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 5661af01302..36ea5e013e4 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -1,60 +1,49 @@ - page_title "Container Registry" -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 +%section + .settings-header + %h4 = page_title %p - With the Docker Container Registry integrated into GitLab, every project - can have its own space to store its Docker images. + = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.') %p.append-bottom-0 = succeed '.' do - Learn more about - = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank' + = s_('ContainerRegistry|Learn more about') + = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank' + .row.registry-placeholder.prepend-bottom-10 + .col-lg-12 + #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } - .col-lg-9 - .panel.panel-default - .panel-heading - %h4.panel-title - How to use the Container Registry - .panel-body - %p - First log in to GitLab’s Container Registry using your GitLab username - and password. If you have - = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank' - you need to use a - = succeed ':' do - = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank' - %pre - docker login #{Gitlab.config.registry.host_port} - %br - %p - Once you log in, you’re free to create and upload a container image - using the common - %code build - and - %code push - commands: - %pre - :plain - docker build -t #{escape_once(@project.container_registry_url)} . - docker push #{escape_once(@project.container_registry_url)} + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('registry_list') - %hr - %h5.prepend-top-default - Use different image names - %p.light - GitLab supports up to 3 levels of image names. The following - examples of images are valid for your project: - %pre - :plain - #{escape_once(@project.container_registry_url)}:tag - #{escape_once(@project.container_registry_url)}/optional-image-name:tag - #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag - - - if @images.blank? - %p.settings-message.text-center.append-bottom-default - No container images stored for this project. Add one by following the - instructions above. - - else - = render partial: 'image', collection: @images + .row.prepend-top-10 + .col-lg-12 + .panel.panel-default + .panel-heading + %h4.panel-title + = s_('ContainerRegistry|How to use the Container Registry') + .panel-body + %p + - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank') + - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank') + = s_('ContainerRegistry|First log in to GitLab’s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token } + %pre + docker login #{Gitlab.config.registry.host_port} + %br + %p + = s_('ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe } + %pre + :plain + docker build -t #{escape_once(@project.container_registry_url)} . + docker push #{escape_once(@project.container_registry_url)} + %hr + %h5.prepend-top-default + = s_('ContainerRegistry|Use different image names') + %p.light + = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:') + %pre + :plain + #{escape_once(@project.container_registry_url)}:tag + #{escape_once(@project.container_registry_url)}/optional-image-name:tag + #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index f819f2addaa..0cc6674842a 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -12,7 +12,7 @@ = webpack_bundle_tag 'repo' %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - - if show_auto_devops_callout?(@project) + - if show_auto_devops_callout?(@project) && !show_new_repo? = render 'shared/auto_devops_callout' = render 'projects/last_push' = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index bc1ab5065e4..9ee09262324 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -29,13 +29,13 @@ commit.id, index == 0) do = truncate_sha(commit.id) %td - = commit.author.name + = commit.author_name %td = commit.message %td #{time_ago_with_tooltip(version.authored_date)} %td %strong - = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + = version.format = render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 62c18cc4582..de15fc99eda 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -11,7 +11,7 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author.name}</strong>" }).html_safe + = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe #{time_ago_with_tooltip(@page.commit.authored_date)} .nav-controls diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index e415ec64c38..b8b1f4ca42f 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -1,9 +1,9 @@ - type = impersonation ? "impersonation" : "personal access" %h5.prepend-top-0 - Add a #{type} Token + Add a #{type} token %p.profile-settings-content - Pick a name for the application, and we'll give you a unique #{type} Token. + Pick a name for the application, and we'll give you a unique #{type} token. = form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f| diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 1dfe380db16..4b9af78bc1a 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -7,7 +7,7 @@ = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do = icon('caret-down', class: 'toggle-icon') - %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } } + %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } } %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } } %button.btn.btn-transparent = icon('check', class: 'icon') diff --git a/changelogs/unreleased/26890-fix-default-branches-sorting.yml b/changelogs/unreleased/26890-fix-default-branches-sorting.yml new file mode 100644 index 00000000000..cf7060190b3 --- /dev/null +++ b/changelogs/unreleased/26890-fix-default-branches-sorting.yml @@ -0,0 +1,5 @@ +--- +title: Fix the default branches sorting to actually be 'Last updated' +merge_request: 14295 +author: +type: fixed diff --git a/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml b/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml new file mode 100644 index 00000000000..727f3cecd52 --- /dev/null +++ b/changelogs/unreleased/33493-attempt-to-link-saml-users-to-ldap-by-email.yml @@ -0,0 +1,5 @@ +--- +title: Link SAML users to LDAP by email. +merge_request: 14216 +author: +type: changed diff --git a/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml new file mode 100644 index 00000000000..cea6cb2e48b --- /dev/null +++ b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml @@ -0,0 +1,5 @@ +--- +title: Re-arrange <script> tags before <template> tags in .vue files +merge_request: 14671 +author: +type: changed diff --git a/changelogs/unreleased/36670-remove-edit-form.yml b/changelogs/unreleased/36670-remove-edit-form.yml new file mode 100644 index 00000000000..4e80b685f67 --- /dev/null +++ b/changelogs/unreleased/36670-remove-edit-form.yml @@ -0,0 +1,5 @@ +--- +title: Remove the ability to visit the issue edit form directly +merge_request: 14523 +author: +type: removed diff --git a/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml new file mode 100644 index 00000000000..3d3efcdbcc6 --- /dev/null +++ b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml @@ -0,0 +1,5 @@ +--- +title: Hide close MR button after merge without reloading page +merge_request: 14122 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/37970-timestamped-ci.yml b/changelogs/unreleased/37970-timestamped-ci.yml new file mode 100644 index 00000000000..2a4797f069a --- /dev/null +++ b/changelogs/unreleased/37970-timestamped-ci.yml @@ -0,0 +1,5 @@ +--- +title: Strip gitlab-runner section markers in build trace HTML view +merge_request: 14393 +author: +type: added diff --git a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml new file mode 100644 index 00000000000..579c247c4c2 --- /dev/null +++ b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml @@ -0,0 +1,5 @@ +--- +title: Fix bottom spacing for dropdowns that open upwards +merge_request: 14535 +author: +type: fixed diff --git a/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml new file mode 100644 index 00000000000..768e296fcd7 --- /dev/null +++ b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml @@ -0,0 +1,6 @@ +--- +title: Does not check if an invariant hashed storage path exists on disk when renaming + projects. +merge_request: 14428 +author: +type: fixed diff --git a/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml b/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml deleted file mode 100644 index f3c39827590..00000000000 --- a/changelogs/unreleased/38319-nomethoderror-undefined-method-sha-for-nil-nilclass.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 error on merged merge requests when GitLab is restored from a backup -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml new file mode 100644 index 00000000000..419e9295d32 --- /dev/null +++ b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml @@ -0,0 +1,5 @@ +--- +title: Use explicit boolean true attribute for show-disabled-button in Vue files +merge_request: 14672 +author: +type: fixed diff --git a/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml b/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml new file mode 100644 index 00000000000..974adb9ed28 --- /dev/null +++ b/changelogs/unreleased/38502-fix-nav-dropdown-close-animation.yml @@ -0,0 +1,5 @@ +--- +title: Fix navigation dropdown close animation on mobile screens +merge_request: 14649 +author: +type: fixed diff --git a/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml b/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml new file mode 100644 index 00000000000..49d0671233a --- /dev/null +++ b/changelogs/unreleased/38635-fix-gitlab-check-git-ssh-config.yml @@ -0,0 +1,5 @@ +--- +title: Whitelist authorized_keys.lock in the gitlab:check rake task +merge_request: 14624 +author: +type: fixed diff --git a/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml new file mode 100644 index 00000000000..7f098c8f60c --- /dev/null +++ b/changelogs/unreleased/add-ci-builds-index-for-jobscontroller.yml @@ -0,0 +1,5 @@ +--- +title: Change index on ci_builds to optimize Jobs Controller +merge_request: +author: +type: other diff --git a/changelogs/unreleased/add-labels-template-index.yml b/changelogs/unreleased/add-labels-template-index.yml new file mode 100644 index 00000000000..5f66c4ce181 --- /dev/null +++ b/changelogs/unreleased/add-labels-template-index.yml @@ -0,0 +1,5 @@ +--- +title: Add (partial) index on Labels.template +merge_request: +author: +type: other diff --git a/changelogs/unreleased/close-issue-by-implements.yml b/changelogs/unreleased/close-issue-by-implements.yml new file mode 100644 index 00000000000..fe36ce3f7aa --- /dev/null +++ b/changelogs/unreleased/close-issue-by-implements.yml @@ -0,0 +1,5 @@ +--- +title: "Add \"implements\" to the default issue closing message regex" +merge_request: 14612 +author: Guilherme Vieira +type: added diff --git a/changelogs/unreleased/commit-row-avatar-align-top.yml b/changelogs/unreleased/commit-row-avatar-align-top.yml new file mode 100644 index 00000000000..aa5ab770bd8 --- /dev/null +++ b/changelogs/unreleased/commit-row-avatar-align-top.yml @@ -0,0 +1,5 @@ +--- +title: Fixed commit avatars being centered vertically +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-copy-parallel-diff.yml b/changelogs/unreleased/dm-copy-parallel-diff.yml new file mode 100644 index 00000000000..96a65007661 --- /dev/null +++ b/changelogs/unreleased/dm-copy-parallel-diff.yml @@ -0,0 +1,5 @@ +--- +title: Only copy old/new code when selecting left/right side of parallel diff +merge_request: +author: +type: added diff --git a/changelogs/unreleased/dm-pat-revoke.yml b/changelogs/unreleased/dm-pat-revoke.yml new file mode 100644 index 00000000000..32ac66056d5 --- /dev/null +++ b/changelogs/unreleased/dm-pat-revoke.yml @@ -0,0 +1,5 @@ +--- +title: Set default scope on PATs that don't have one set to allow them to be revoked +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/docs-add-summary-about-project-archiving.yml b/changelogs/unreleased/docs-add-summary-about-project-archiving.yml new file mode 100644 index 00000000000..cc1b48a682d --- /dev/null +++ b/changelogs/unreleased/docs-add-summary-about-project-archiving.yml @@ -0,0 +1,5 @@ +--- +title: Add documentation to summarise project archiving +merge_request: 14650 +author: +type: other diff --git a/changelogs/unreleased/ff_port_from_ee.yml b/changelogs/unreleased/ff_port_from_ee.yml new file mode 100644 index 00000000000..e1cb7804a47 --- /dev/null +++ b/changelogs/unreleased/ff_port_from_ee.yml @@ -0,0 +1,5 @@ +--- +title: Move Custom merge methods from EE +merge_request: +author: +type: added diff --git a/changelogs/unreleased/fix-gpg-case-insensitive.yml b/changelogs/unreleased/fix-gpg-case-insensitive.yml new file mode 100644 index 00000000000..744ec00a4a8 --- /dev/null +++ b/changelogs/unreleased/fix-gpg-case-insensitive.yml @@ -0,0 +1,5 @@ +--- +title: Compare email addresses case insensitively when verifying GPG signatures +merge_request: 14376 +author: Tim Bishop +type: fixed diff --git a/changelogs/unreleased/fix-kubectl-180.yml b/changelogs/unreleased/fix-kubectl-180.yml new file mode 100644 index 00000000000..beb71cecd57 --- /dev/null +++ b/changelogs/unreleased/fix-kubectl-180.yml @@ -0,0 +1,5 @@ +--- +title: 'Kubernetes integration: ensure v1.8.0 compatibility' +merge_request: 14635 +author: +type: fixed diff --git a/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml new file mode 100644 index 00000000000..13ec113167f --- /dev/null +++ b/changelogs/unreleased/gem-sm-bump-google-api-client-gem-from-0-8-6-to-0-13-6.yml @@ -0,0 +1,5 @@ +--- +title: Bump google-api-client Gem from 0.8.6 to 0.13.6 +merge_request: +author: +type: other diff --git a/changelogs/unreleased/merge-request-notes-performance.yml b/changelogs/unreleased/merge-request-notes-performance.yml new file mode 100644 index 00000000000..6cf7a5047df --- /dev/null +++ b/changelogs/unreleased/merge-request-notes-performance.yml @@ -0,0 +1,5 @@ +--- +title: Use a UNION ALL for getting merge request notes +merge_request: +author: +type: other diff --git a/changelogs/unreleased/mr-widget-merged-date-tooltip.yml b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml new file mode 100644 index 00000000000..ea22993ff52 --- /dev/null +++ b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Fixed merge request widget merged & closed date tooltip text +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml b/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml new file mode 100644 index 00000000000..69695e403a9 --- /dev/null +++ b/changelogs/unreleased/rd-fix-case-sensative-email-conf-signup.yml @@ -0,0 +1,5 @@ +--- +title: Fix case sensitive email confirmation on signup +merge_request: 14606 +author: robdel12 +type: fixed diff --git a/changelogs/unreleased/sh-fix-import-repos.yml b/changelogs/unreleased/sh-fix-import-repos.yml new file mode 100644 index 00000000000..5764b3bdc01 --- /dev/null +++ b/changelogs/unreleased/sh-fix-import-repos.yml @@ -0,0 +1,5 @@ +--- +title: Fix gitlab-rake gitlab:import:repos task failing +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-thread-safe-markdown.yml b/changelogs/unreleased/sh-thread-safe-markdown.yml new file mode 100644 index 00000000000..af7d9d58a9f --- /dev/null +++ b/changelogs/unreleased/sh-thread-safe-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Make Redcarpet Markdown renderer thread-safe +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/update-pages-0-6.yml b/changelogs/unreleased/update-pages-0-6.yml new file mode 100644 index 00000000000..507bb4d78e9 --- /dev/null +++ b/changelogs/unreleased/update-pages-0-6.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Pages to v0.6.0 +merge_request: 14630 +author: +type: other diff --git a/changelogs/unreleased/winh-sprintf.yml b/changelogs/unreleased/winh-sprintf.yml new file mode 100644 index 00000000000..f8ae5932ae4 --- /dev/null +++ b/changelogs/unreleased/winh-sprintf.yml @@ -0,0 +1,5 @@ +--- +title: Add basic sprintf implementation to JavaScript +merge_request: 14506 +author: +type: other diff --git a/config/application.rb b/config/application.rb index 30117b6a98e..ca2ab83becc 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "test.css" + config.assets.precompile << "locale/**/app.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/environments/test.rb b/config/environments/test.rb index 278144b8943..1edb6fd39b8 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -16,7 +16,7 @@ Rails.application.configure do config.cache_classes = ENV['CACHE_CLASSES'] == 'true' # Configure static asset server for tests with Cache-Control for performance - config.assets.digest = false + config.assets.compile = false if ENV['CI'] config.serve_static_files = true config.static_cache_control = "public, max-age=3600" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 9b496822e93..88771c5f5bb 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -89,7 +89,7 @@ production: &base # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. # Tip: you can test your closing pattern at http://rubular.com. - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' ## Default project features settings default_projects_features: @@ -499,6 +499,8 @@ production: &base # Gitaly settings gitaly: + # Path to the directory containing Gitaly client executables. + client_path: /home/git/gitaly # Default Gitaly authentication token. Can be overriden per storage. Can # be left blank when Gitaly is running locally on a Unix socket, which # is the normal way to deploy Gitaly. @@ -664,7 +666,7 @@ test: gitaly_address: unix:tmp/tests/gitaly/gitaly.socket gitaly: - enabled: true + client_path: tmp/tests/gitaly token: secret backup: path: tmp/tests/backups diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 27c1ecc7b23..a23b3208dab 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -257,7 +257,7 @@ Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled']. Settings.gitlab['password_authentication_enabled'] ||= true if Settings.gitlab['password_authentication_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.__send__(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? -Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? +Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 3aed2136f1b..0ba0d791054 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -36,7 +36,7 @@ Devise.setup do |config| # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [:email, :email_confirmation] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb index 377e5104f9d..49551319435 100644 --- a/config/initializers/gettext_rails_i18n_patch.rb +++ b/config/initializers/gettext_rails_i18n_patch.rb @@ -39,3 +39,17 @@ module GettextI18nRailsJs end end end + +class PoToJson + # This is required to modify the JS locale file output to our import needs + # Overwrites: https://github.com/webhippie/po_to_json/blob/master/lib/po_to_json.rb#L46 + def generate_for_jed(language, overwrite = {}) + @options = parse_options(overwrite.merge(language: language)) + @parsed ||= inject_meta(parse_document) + + generated = build_json_for(build_jed_for(@parsed)) + [ + "window.translations = #{generated};" + ].join(" ") + end +end diff --git a/config/initializers/grpc.rb b/config/initializers/grpc.rb new file mode 100644 index 00000000000..b96962fe7db --- /dev/null +++ b/config/initializers/grpc.rb @@ -0,0 +1,11 @@ +require 'logger' + +GRPC_LOGGER = Logger.new(Rails.root.join('log/grpc.log')) +GRPC_LOGGER.level = ENV['GRPC_LOG_LEVEL'].presence || 'WARN' +GRPC_LOGGER.progname = 'GRPC' + +module GRPC + def self.logger + GRPC_LOGGER + end +end diff --git a/config/routes/project.rb b/config/routes/project.rb index b36d13888cd..70d7673250c 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -271,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do namespace :registry do resources :repository, only: [] do - resources :tags, only: [:destroy], + resources :tags, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_tag_regex } end end diff --git a/config/webpack.config.js b/config/webpack.config.js index 3404715fe30..c515a170d2d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -68,6 +68,7 @@ var config = { prometheus_metrics: './prometheus_metrics', protected_branches: './protected_branches', protected_tags: './protected_tags', + registry_list: './registry/index.js', repo: './repo/index.js', sidebar: './sidebar/sidebar_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', @@ -122,10 +123,6 @@ var config = { } }, { - test: /locale\/\w+\/(.*)\.js$/, - loader: 'exports-loader?locales', - }, - { test: /monaco-editor\/\w+\/vs\/loader\.js$/, use: [ { loader: 'exports-loader', options: 'l.global' }, @@ -200,6 +197,7 @@ var config = { 'pdf_viewer', 'pipelines', 'pipelines_details', + 'registry_list', 'repo', 'schedule_form', 'schedules_index', @@ -222,7 +220,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'webpack_runtime'], + names: ['main', 'common', 'webpack_runtime'], }), // enable scope hoisting diff --git a/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb b/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb new file mode 100644 index 00000000000..3dafdf0fde4 --- /dev/null +++ b/db/migrate/20141126120926_add_merge_request_rebase_enabled_to_projects.rb @@ -0,0 +1,17 @@ +# rubocop:disable all +class AddMergeRequestRebaseEnabledToProjects < 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(:projects, :merge_requests_rebase_enabled, :boolean, default: false) + end + + def down + remove_column(:projects, :merge_requests_rebase_enabled) + end +end diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb new file mode 100644 index 00000000000..6f22641077d --- /dev/null +++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb @@ -0,0 +1,19 @@ +# rubocop:disable all +class AddFastForwardOptionToProject < 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(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) + end + + def down + if column_exists?(:projects, :merge_requests_ff_only_enabled) + remove_column(:projects, :merge_requests_ff_only_enabled) + end + end +end diff --git a/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb new file mode 100644 index 00000000000..c2cb1df2586 --- /dev/null +++ b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCiBuildsIndexForJobscontroller < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_builds, [:project_id, :id] unless index_exists? :ci_builds, [:project_id, :id] + remove_concurrent_index :ci_builds, :project_id if index_exists? :ci_builds, :project_id + end + + def down + add_concurrent_index :ci_builds, :project_id unless index_exists? :ci_builds, :project_id + remove_concurrent_index :ci_builds, [:project_id, :id] if index_exists? :ci_builds, [:project_id, :id] + end +end diff --git a/db/migrate/20170927122209_add_partial_index_for_labels_template.rb b/db/migrate/20170927122209_add_partial_index_for_labels_template.rb new file mode 100644 index 00000000000..c3e5077ba20 --- /dev/null +++ b/db/migrate/20170927122209_add_partial_index_for_labels_template.rb @@ -0,0 +1,45 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPartialIndexForLabelsTemplate < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + + disable_ddl_transaction! + + # Note this is a partial index in Postgres but MySQL will ignore the + # partial index clause. By making it an index on "template" this + # means the index will still accomplish the same goal of optimizing + # a query with "where template = true" on MySQL -- it'll just take + # more space. In this case the number of records with template=true + # is expected to be very small (small enough to display on a single + # web page) so it's ok to filter or sort them without the index + # anyways. + + def up + add_concurrent_index "labels", ["template"], where: "template" + end + + def down + remove_concurrent_index "labels", ["template"], where: "template" + end +end diff --git a/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb new file mode 100644 index 00000000000..ac266c3e22e --- /dev/null +++ b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb @@ -0,0 +1,25 @@ +# rubocop:disable all +class MakeSureFastForwardOptionExists < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + # We had to fix the migration db/migrate/20150827121444_add_fast_forward_option_to_project.rb + # And this is why it's possible that someone has ran the migrations but does + # not have the merge_requests_ff_only_enabled column. This migration makes sure it will + # be added + unless column_exists?(:projects, :merge_requests_ff_only_enabled) + add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) + end + end + + def down + if column_exists?(:projects, :merge_requests_ff_only_enabled) + remove_column(:projects, :merge_requests_ff_only_enabled) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e8e64b9d36b..fa1aad257db 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170928100231) do +ActiveRecord::Schema.define(version: 20171004121444) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -256,7 +256,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree + add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree @@ -730,6 +730,7 @@ ActiveRecord::Schema.define(version: 20170928100231) do add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree + add_index "labels", ["template"], name: "index_labels_on_template", where: "template", using: :btree add_index "labels", ["title"], name: "index_labels_on_title", using: :btree add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree @@ -1217,6 +1218,8 @@ ActiveRecord::Schema.define(version: 20170928100231) do t.integer "storage_version", limit: 2 t.boolean "resolve_outdated_diff_discussions" t.boolean "repository_read_only" + t.boolean "merge_requests_ff_only_enabled", default: false + t.boolean "merge_requests_rebase_enabled", default: false, null: false end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 40099dcc967..e3b10119090 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -32,6 +32,14 @@ prometheus_listen_addr = "localhost:9236" Changes to `/home/git/gitaly/config.toml` are applied when you run `service gitlab restart`. +## Client-side GRPC logs + +Gitaly uses the [gRPC](https://grpc.io/) RPC framework. The Ruby gRPC +client has its own log file which may contain useful information when +you are seeing Gitaly errors. You can control the log level of the +gRPC client with the `GRPC_LOG_LEVEL` environment variable. The +default level is `WARN`. + ## Running Gitaly on its own server > This is an optional way to deploy Gitaly which can benefit GitLab diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md index 796a025b951..b8f9988e3ef 100644 --- a/doc/ci/enable_or_disable_ci.md +++ b/doc/ci/enable_or_disable_ci.md @@ -1,51 +1,46 @@ -## Enable or disable GitLab CI +## Enable or disable GitLab CI/CD -_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md) +To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md) file present at the root directory of your project and a [runner](runners/README.md) properly set up. You can read our -[quick start guide](quick_start/README.md) to get you started._ +[quick start guide](quick_start/README.md) to get you started. -If you are using an external CI server like Jenkins or Drone CI, it is advised -to disable GitLab CI in order to not have any conflicts with the commits status +If you are using an external CI/CD server like Jenkins or Drone CI, it is advised +to disable GitLab CI/CD in order to not have any conflicts with the commits status API. --- -GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project. -Disabling GitLab CI in a project does not delete any previous jobs. -In fact, the `/pipelines` and `/builds` pages can still be accessed, although +GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project. +Disabling GitLab CI/CD in a project does not delete any previous jobs. +In fact, the `/pipelines` and `/jobs` pages can still be accessed, although it's hidden from the left sidebar menu. -GitLab CI is enabled by default on new installations and can be disabled either +GitLab CI/CD is enabled by default on new installations and can be disabled either individually under each project's settings, or site-wide by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. ### Per-project user setting -The setting to enable or disable GitLab CI can be found with the name **Pipelines** -under the **Sharing & Permissions** area of a project's settings along with -**Merge Requests**. Choose one of **Disabled**, **Only team members** and -**Everyone with access** and hit **Save changes** for the settings to take effect. +The setting to enable or disable GitLab CI/CD can be found under your project's +**Settings > General > Permissions**. Choose one of "Disabled", "Only team members" +or "Everyone with access" and hit **Save changes** for the settings to take effect. -![Sharing & Permissions settings](img/permissions_settings.png) +![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png) ---- - -### Site-wide administrator setting +### Site-wide admin setting -You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml` +You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. Two things to note: -1. Disabling GitLab CI, will affect only newly-created projects. Projects that +1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that had it enabled prior to this modification, will work as before. -1. Even if you disable GitLab CI, users will still be able to enable it in the +1. Even if you disable GitLab CI/CD, users will still be able to enable it in the project's settings. ---- - For installations from source, open `gitlab.yml` with your editor and set `builds` to `false`: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index acd5682841a..c03e16b1b38 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your servers. If you have a deployment service such as [Kubernetes][kubernetes-service] enabled for your project, you can use it to assist with your deployments, and -can even access a web terminal for your environment from within GitLab! +can even access a [web terminal](#web-terminals) for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -119,7 +119,7 @@ where you can find information of the last deployment status of an environment. Here's how the Environments page looks so far. -![Staging environment view](img/environments_available_staging.png) +![Environment view](img/environments_available.png) There's a bunch of information there, specifically you can see: @@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views. | Pipelines | Single pipeline | Environments | Deployments | jobs | | --------- | ----------------| ------------ | ----------- | -------| -| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) | +| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_jobs.png) | Clicking on the play button in either of these places will trigger the `deploy_prod` job, and the deployment will be recorded under a new @@ -402,7 +402,7 @@ places within GitLab. | In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button | | -------------------- | ------------ | ----------- | -| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) | +| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_available.png) | ![Environment URL in deployments](img/deployments_view.png) | If a merge request is eventually merged to the default branch (in our case `master`) and that branch also deploys to an environment (in our case `staging` @@ -574,7 +574,7 @@ Once configured, GitLab will attempt to retrieve [supported performance metrics] environment which has had a successful deployment. If monitoring data was successfully retrieved, a Monitoring button will appear for each environment. -![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) +![Environment Detail with Metrics](img/deployments_view.png) Clicking on the Monitoring button will display a new page, showing up to the last 8 hours of performance data. It may take a minute or two for data to appear @@ -593,10 +593,11 @@ Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes service][kubernetes-service], GitLab can open +the [Kubernetes service][kubernetes-service]), GitLab can open a terminal session to your environment! This is a very powerful feature that allows you to debug issues without leaving the comfort of your web browser. To -enable it, just follow the instructions given in the service documentation. +enable it, just follow the instructions given in the service integration +documentation. Once enabled, your environments will gain a "terminal" button: diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png Binary files differdeleted file mode 100644 index 2d7eec8a949..00000000000 --- a/doc/ci/img/builds_tab.png +++ /dev/null diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png Binary files differindex 7ded0c97b72..436fed5f465 100644 --- a/doc/ci/img/deployments_view.png +++ b/doc/ci/img/deployments_view.png diff --git a/doc/ci/img/environments_available.png b/doc/ci/img/environments_available.png Binary files differnew file mode 100644 index 00000000000..2991a309655 --- /dev/null +++ b/doc/ci/img/environments_available.png diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png Binary files differdeleted file mode 100644 index 5c031ad0d9d..00000000000 --- a/doc/ci/img/environments_available_staging.png +++ /dev/null diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png Binary files differindex 0f42b368c5b..45124b3d8d8 100644 --- a/doc/ci/img/environments_dynamic_groups.png +++ b/doc/ci/img/environments_dynamic_groups.png diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png Binary files differdeleted file mode 100644 index 44010f6aa6f..00000000000 --- a/doc/ci/img/environments_link_url.png +++ /dev/null diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png Binary files differdeleted file mode 100644 index 4f90143527a..00000000000 --- a/doc/ci/img/environments_link_url_deployments.png +++ /dev/null diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png Binary files differindex 64f134e0b0d..7ce46063062 100644 --- a/doc/ci/img/environments_link_url_mr.png +++ b/doc/ci/img/environments_link_url_mr.png diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png Binary files differdeleted file mode 100644 index e7cf63a1031..00000000000 --- a/doc/ci/img/environments_manual_action_builds.png +++ /dev/null diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png Binary files differindex 2b3f6f3edad..93beaa0de54 100644 --- a/doc/ci/img/environments_manual_action_deployments.png +++ b/doc/ci/img/environments_manual_action_deployments.png diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png Binary files differindex e0c07604e7f..9490be63f14 100644 --- a/doc/ci/img/environments_manual_action_environments.png +++ b/doc/ci/img/environments_manual_action_environments.png diff --git a/doc/ci/img/environments_manual_action_jobs.png b/doc/ci/img/environments_manual_action_jobs.png Binary files differnew file mode 100644 index 00000000000..9ae223cf77f --- /dev/null +++ b/doc/ci/img/environments_manual_action_jobs.png diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png Binary files differindex 82bbae88027..129e44f6fb0 100644 --- a/doc/ci/img/environments_manual_action_pipelines.png +++ b/doc/ci/img/environments_manual_action_pipelines.png diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png Binary files differindex 36337cb1870..1eeb4379eb7 100644 --- a/doc/ci/img/environments_manual_action_single_pipeline.png +++ b/doc/ci/img/environments_manual_action_single_pipeline.png diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png Binary files differindex 7bff84362a3..4bb643d708f 100644 --- a/doc/ci/img/environments_mr_review_app.png +++ b/doc/ci/img/environments_mr_review_app.png diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png Binary files differindex 6f05b2aa343..061bb7c3c87 100644 --- a/doc/ci/img/environments_terminal_button_on_index.png +++ b/doc/ci/img/environments_terminal_button_on_index.png diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png Binary files differindex 9469fab99ab..4d24304bc93 100644 --- a/doc/ci/img/environments_terminal_button_on_show.png +++ b/doc/ci/img/environments_terminal_button_on_show.png diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png Binary files differdeleted file mode 100644 index 821352188ef..00000000000 --- a/doc/ci/img/environments_view.png +++ /dev/null diff --git a/doc/ci/img/permissions_settings.png b/doc/ci/img/permissions_settings.png Binary files differdeleted file mode 100644 index 1454c75fd24..00000000000 --- a/doc/ci/img/permissions_settings.png +++ /dev/null diff --git a/doc/ci/img/prometheus_environment_detail_with_metrics.png b/doc/ci/img/prometheus_environment_detail_with_metrics.png Binary files differdeleted file mode 100644 index 214b10624a9..00000000000 --- a/doc/ci/img/prometheus_environment_detail_with_metrics.png +++ /dev/null diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index ebcb92b5db1..17839cbaef1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -149,14 +149,15 @@ script: ## Secret variables ->**Notes:** -- This feature requires GitLab Runner 0.4.0 or higher. -- Group-level secret variables added in GitLab 9.4. -- Be aware that secret variables are not masked, and their values can be shown - in the job logs if explicitly asked to do so. If your project is public or - internal, you can set the pipelines private from your project's Pipelines - settings. Follow the discussion in issue [#13784][ce-13784] for masking the - secret variables. +NOTE: **Note:** +Group-level secret variables were added in GitLab 9.4. + +CAUTION: **Important:** +Be aware that secret variables are not masked, and their values can be shown +in the job logs if explicitly asked to do so. If your project is public or +internal, you can set the pipelines private from your [project's Pipelines +settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). +Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. GitLab CI allows you to define per-project or per-group secret variables that are set in the pipeline environment. The secret variables are stored out of @@ -171,6 +172,8 @@ Likewise, group-level secret variables can be added by going to your group's **Settings > CI/CD**, then finding the section called **Secret variables**. Any variables of [subgroups] will be inherited recursively. +![Secret variables](img/secret_variables.png) + Once you set them, they will be available for all subsequent pipelines. You can also [protect your variables](#protected-secret-variables). @@ -202,7 +205,7 @@ are set in the build environment. These variables are only defined for the project services that you are using to learn which variables they define. An example project service that defines deployment variables is -[Kubernetes Service](../../user/project/integrations/kubernetes.md). +[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables). ## Debug tracing @@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token" export CI_REGISTRY_PASSWORD="longalfanumstring" ``` -[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 +[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables" [eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium" [envs]: ../environments.md [protected branches]: ../../user/project/protected_branches.md diff --git a/doc/ci/variables/img/secret_variables.png b/doc/ci/variables/img/secret_variables.png Binary files differnew file mode 100644 index 00000000000..f70935069d9 --- /dev/null +++ b/doc/ci/variables/img/secret_variables.png diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index c8d23609280..77ae6d2a0ea 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -470,7 +470,25 @@ On those a default key should not be provided. ``` #### Ordering -1. Order for a Vue Component: + +1. Tag order in `.vue` file + + ``` + <script> + // ... + </script> + + <template> + // ... + </template> + + // We don't use scoped styles but there are few instances of this + <style> + // ... + </style> + ``` + +1. Properties in a Vue Component: 1. `name` 1. `props` 1. `mixins` @@ -490,6 +508,7 @@ On those a default key should not be provided. 1. `beforeDestroy` 1. `destroyed` + #### Vue and Bootstrap 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 2607353782a..277e0cd5f00 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -428,7 +428,7 @@ is a good example of this pattern. ## Style guide -Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) +Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js) for best practices while writing your Vue components and templates. ## Testing Vue Components diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md index bd0ef39ca62..29c8941a8f7 100644 --- a/doc/development/i18n_guide.md +++ b/doc/development/i18n_guide.md @@ -183,13 +183,20 @@ aren't in the message with id `1 pipeline`. ### Interpolation -- In Ruby/HAML: +- In Ruby/HAML (see [sprintf]): ```ruby _("Hello %{name}") % { name: 'Joe' } ``` -- In JavaScript: Not supported at this moment. +- In JavaScript: Only named parameters are supported (see also [#37992]): + + ```javascript + __("Hello %{name}") % { name: 'Joe' } + ``` + +[sprintf]: http://ruby-doc.org/core/Kernel.html#method-i-sprintf +[#37992]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 ### Plurals diff --git a/doc/development/testing.md b/doc/development/testing.md index d856b003353..4d5b90de6fc 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -302,7 +302,7 @@ range of inputs, might look like this: ```ruby describe "#==" do - using Rspec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax let(:project1) { create(:project) } let(:project2) { create(:project) } diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 5dae4bcc905..d190ee1b0ff 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -39,6 +39,12 @@ When information is updating in place, a quick, subtle animation is needed. The ![Quick update animation](img/animation-quickupdate.gif) +### Skeleton loading + +Skeleton loading is explained in the [component section](components.html#skeleton-loading) of the UX guide. It includes a horizontally pulsating animation that shows motion as if it's growing. It's timing is a slower `linear 1s`. + +![Skeleton loading animation](img/skeleton-loading.gif) + ### Moving transitions When elements move on screen, there should be a quick animation so it is clear to users what moved where. The timing of this animation differs based on the amount of movement and change. Consider animations between `200ms` and `400ms`. @@ -51,7 +57,9 @@ View the [interactive example](http://codepen.io/awhildy/full/ALyKPE/) here. ![Reorder animation](img/animation-reorder.gif) #### Autoscroll the page + Another example of a moving transition is when you have to autoscroll the page to keep an active element visible. View the [interactive example](http://codepen.io/awhildy/full/PbxgVo/) here. -![Autoscroll animation](img/animation-autoscroll.gif)
\ No newline at end of file + +![Autoscroll animation](img/animation-autoscroll.gif) diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index ac7c1b6207d..986b796437b 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -204,6 +204,25 @@ Cover blocks are generally used to create a heading element for a page, such as --- +## Skeleton loading + +Skeleton loading is a way to convey to the user what kind of content is currently being loaded. It's a paradigm with which content can independently and asynchronously be loaded, while still adhering to the structure and look of the completely loaded view. + +### Requirements + +* A skeleton should represent an organism in a recognisable way +* Atom elements within organisms (for reference see this article on [atomic design methodology](http://atomicdesign.bradfrost.com/chapter-2/)) may be represented in a maximum of 3 repetitions, if applicable. +* Skeletons should only be presented in grayscale using the HEX colors: `#fafafa` or `#ffffff` (except for shadows) +* Animate the grey atoms in a pulsating way to show motion, as if "loading". The pulse animation transitions colors horizontally from left to right, starting with `#f2f2f2` to `#fafafa`. + +![Skeleton loading animation](img/skeleton-loading.gif) + +### Usage + +Skeleton loading can replace any existing UI elements for the period in which they are loaded and should aim for maintaining a similar structure visually. + +--- + ## Panels > TODO: Catalog how we are currently using panels and rationalize how they relate to alerts diff --git a/doc/development/ux_guide/img/skeleton-loading.gif b/doc/development/ux_guide/img/skeleton-loading.gif Binary files differnew file mode 100644 index 00000000000..5877139171d --- /dev/null +++ b/doc/development/ux_guide/img/skeleton-loading.gif diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 5c615daf464..2c4dfcff4a6 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -17,25 +17,25 @@ have its own space to store its Docker images. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. ---- - ## Enable the Container Registry for your project +NOTE: **Note:** +If you cannot find the Container Registry entry under your project's settings, +that means that it is not enabled in your GitLab instance. Ask your administrator +to enable it. + 1. First, ask your system administrator to enable GitLab Container Registry following the [administration documentation](../../administration/container_registry.md). If you are using GitLab.com, this is enabled by default so you can start using the Registry immediately. - -1. Go to your project's settings and enable the **Container Registry** feature - on your project. For new projects this might be enabled by default. For - existing projects (prior GitLab 8.8), you will have to explicitly enable it. - - ![Enable Container Registry](img/container_registry_enable.png) - +1. Go to your [project's General settings](settings/index.md#sharing-and-permissions) + and enable the **Container Registry** feature on your project. For new + projects this might be enabled by default. For existing projects + (prior GitLab 8.8), you will have to explicitly enable it. 1. Hit **Save changes** for the changes to take effect. You should now be able - to see the **Registry** link in the project menu. + to see the **Registry** link in the sidebar. - ![Container Registry tab](img/container_registry_tab.png) +![Container Registry](img/container_registry.png) ## Build and push images diff --git a/doc/user/project/img/container_registry.png b/doc/user/project/img/container_registry.png Binary files differnew file mode 100644 index 00000000000..abbaf838538 --- /dev/null +++ b/doc/user/project/img/container_registry.png diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png Binary files differdeleted file mode 100644 index d067a8be1ca..00000000000 --- a/doc/user/project/img/container_registry_enable.png +++ /dev/null diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png Binary files differdeleted file mode 100644 index a85237271d9..00000000000 --- a/doc/user/project/img/container_registry_tab.png +++ /dev/null diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex cf7f519f783..5f6dc9e4e8b 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png Binary files differindex c6b17ada40e..3666dbb87ab 100644 --- a/doc/user/project/img/issue_board_move_issue_card_list.png +++ b/doc/user/project/img/issue_board_move_issue_card_list.png diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png Binary files differdeleted file mode 100644 index badfbed0bbe..00000000000 --- a/doc/user/project/img/labels_assign_label_in_new_issue.png +++ /dev/null diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png Binary files differindex 474953d565b..7934e3bfb5e 100644 --- a/doc/user/project/img/labels_default.png +++ b/doc/user/project/img/labels_default.png diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png Binary files differindex 3aca77f0070..6a1ebfc2ecb 100644 --- a/doc/user/project/img/labels_filter.png +++ b/doc/user/project/img/labels_filter.png diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png Binary files differindex 5609a1f6d7f..419e555e709 100644 --- a/doc/user/project/img/labels_filter_by_priority.png +++ b/doc/user/project/img/labels_filter_by_priority.png diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png Binary files differindex b44b4bd296d..e26425d0188 100644 --- a/doc/user/project/img/labels_new_label.png +++ b/doc/user/project/img/labels_new_label.png diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png Binary files differindex 3e888f36364..d602a3c90ec 100644 --- a/doc/user/project/img/labels_prioritize.png +++ b/doc/user/project/img/labels_prioritize.png diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png Binary files differindex 1aa7efc36f1..aa4d4452c87 100644 --- a/doc/user/project/img/project_repository_settings.png +++ b/doc/user/project/img/project_repository_settings.png diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index e2cc67726e0..96a5a23ee13 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -12,6 +12,8 @@ Other interesting links: - [GitLab Issue Board landing page on about.gitlab.com][landing] - [YouTube video introduction to Issue Boards][youtube] +![GitLab Issue Board](img/issue_board.png) + ## Overview The Issue Board builds on GitLab's existing @@ -89,10 +91,6 @@ two defaults: - **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left. - **Closed** (default): shows all closed issues. Always appears on the very right. -![GitLab Issue Board](img/issue_board.png) - ---- - In short, here's a list of actions you can take in an Issue Board: - [Create a new list](#creating-a-new-list). diff --git a/doc/user/project/issues/img/button_close_issue.png b/doc/user/project/issues/img/button_close_issue.png Binary files differindex 8fb2e23f58a..05d257ce9bf 100644 --- a/doc/user/project/issues/img/button_close_issue.png +++ b/doc/user/project/issues/img/button_close_issue.png diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png Binary files differindex 5d20e8cbc89..bba964076d0 100644 --- a/doc/user/project/issues/img/group_issues_list_view.png +++ b/doc/user/project/issues/img/group_issues_list_view.png diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png Binary files differindex 1759b28a9ef..87b1016cc76 100644 --- a/doc/user/project/issues/img/issue_board.png +++ b/doc/user/project/issues/img/issue_board.png diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png Binary files differindex c63229a4af2..0e4c8df897b 100644 --- a/doc/user/project/issues/img/issue_template.png +++ b/doc/user/project/issues/img/issue_template.png diff --git a/doc/user/project/issues/img/issues_main_view.png b/doc/user/project/issues/img/issues_main_view.png Binary files differindex 4faa42e40ee..a929916c682 100644 --- a/doc/user/project/issues/img/issues_main_view.png +++ b/doc/user/project/issues/img/issues_main_view.png diff --git a/doc/user/project/issues/img/issues_main_view_numbered.jpg b/doc/user/project/issues/img/issues_main_view_numbered.jpg Binary files differindex 4b5d7fba459..b4b68476d24 100644 --- a/doc/user/project/issues/img/issues_main_view_numbered.jpg +++ b/doc/user/project/issues/img/issues_main_view_numbered.jpg diff --git a/doc/user/project/issues/img/new_issue.png b/doc/user/project/issues/img/new_issue.png Binary files differindex e72ac49d6b9..07d65a93070 100644 --- a/doc/user/project/issues/img/new_issue.png +++ b/doc/user/project/issues/img/new_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_issue_board.png b/doc/user/project/issues/img/new_issue_from_issue_board.png Binary files differindex 9c2b3ff50fa..da892eff0a6 100644 --- a/doc/user/project/issues/img/new_issue_from_issue_board.png +++ b/doc/user/project/issues/img/new_issue_from_issue_board.png diff --git a/doc/user/project/issues/img/new_issue_from_open_issue.png b/doc/user/project/issues/img/new_issue_from_open_issue.png Binary files differindex 2aed5372830..c6f3f0617ab 100644 --- a/doc/user/project/issues/img/new_issue_from_open_issue.png +++ b/doc/user/project/issues/img/new_issue_from_open_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png Binary files differindex cddf36b7457..4b9535f6b15 100644 --- a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png +++ b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png diff --git a/doc/user/project/issues/img/new_issue_from_tracker_list.png b/doc/user/project/issues/img/new_issue_from_tracker_list.png Binary files differindex 7e5413f0b7d..66793cb44fa 100644 --- a/doc/user/project/issues/img/new_issue_from_tracker_list.png +++ b/doc/user/project/issues/img/new_issue_from_tracker_list.png diff --git a/doc/user/project/issues/img/project_issues_list_view.png b/doc/user/project/issues/img/project_issues_list_view.png Binary files differindex 2fcc9e8d9da..584a81aab8a 100644 --- a/doc/user/project/issues/img/project_issues_list_view.png +++ b/doc/user/project/issues/img/project_issues_list_view.png diff --git a/doc/user/project/issues/img/sidebar_move_issue.png b/doc/user/project/issues/img/sidebar_move_issue.png Binary files differindex 111f7861364..1e688cec894 100644 --- a/doc/user/project/issues/img/sidebar_move_issue.png +++ b/doc/user/project/issues/img/sidebar_move_issue.png diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 8ec7adad172..21a2e1213ec 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**. The first time you visit this page, you'll notice that there are no labels created yet. -![Generate new labels](img/labels_generate.png) - Creating a new label from scratch is as easy as pressing the **New label** button. From there on you can choose the name, give it an optional description, a color and you are set. @@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label. --- -## Default Labels - -It's possible to populate the labels for your project from a set of predefined labels. - -### Generate GitLab's predefined label set +## Default labels -![Generate new labels](img/labels_generate.png) +The very first time you visit the labels area, it's gonna be empty. In that +case, it's possible to populate the labels for your project from a set of +predefined labels. Click the link to 'Generate a default set of labels' and GitLab will -generate a set of predefined labels for you. There are 8 default generated labels -in total and you can see them in the screenshot below. - -![Default generated labels](img/labels_default.png) +generate them for you. There are 8 default generated labels in total: ---- +- bug +- confirmed +- critical +- discussion +- documentation +- enhancement +- suggestion +- support ## Labels Overview @@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels that are relevant to you. You’ll notice it’ll be much easier to focus on what’s important. -## Create a new label right from the issue tracker - -> Introduced in GitLab 8.6. +## Create a new label when inside an issue -There are times when you are already in the issue tracker searching for a +There are times when you are already inside an issue searching to assign a label, only to realize it doesn't exist. Instead of going to the **Labels** page and being distracted from your original purpose, you can create new labels on the fly. -Select **Create new** from the labels dropdown list, provide a name, pick a -color and hit **Create**. +Expand the issue sidebar and select **Create new label** from the labels dropdown +list. Provide a name, pick a color and hit **Create**. The new label will be +ready to used right away! -![Create new label on the fly](img/labels_new_label_on_the_fly_create.png) ![New label on the fly](img/labels_new_label_on_the_fly.png) ## Assigning labels to issues and merge requests There are generally two ways to assign a label to an issue or merge request. -You can assign a label when you first create or edit an issue or merge request. - -![Assign label in new issue](img/labels_assign_label_in_new_issue.png) - ---- +The first one is to assign a label when you first create or edit an issue or +merge request. The second way is by using the right sidebar when inside an issue or merge request. Expand it and hit **Edit** in the labels area. Start typing the name diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 64b94d81024..22ef11e4049 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -2,24 +2,19 @@ > [Introduced][ce-3514] in GitLab 8.7. ---- - GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] -with introducing a **Cherry-pick** button in Merge Requests and commit details. +with introducing a **Cherry-pick** button in merge requests and commit details. -## Cherry-picking a Merge Request +## Cherry-picking a merge request -After the Merge Request has been merged, a **Cherry-pick** button will be available -to cherry-pick the changes introduced by that Merge Request: +After the merge request has been merged, a **Cherry-pick** button will be available +to cherry-pick the changes introduced by that merge request. ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) ---- - -You can cherry-pick the changes directly into the selected branch or you can opt to -create a new Merge Request with the cherry-pick changes: - -![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png) +After you click that button, a modal will appear where you can choose to +cherry-pick the changes directly into the selected branch or you can opt to +create a new merge request with the cherry-pick changes ## Cherry-picking a Commit @@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page: ![Cherry-pick commit](img/cherry_pick_changes_commit.png) ---- - -Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes -directly into the target branch or create a new Merge Request to cherry-pick the -changes: - -![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png) - ---- +Similar to cherry-picking a merge request, you can opt to cherry-pick the changes +directly into the target branch or create a new merge request to cherry-pick the +changes. Please note that when cherry-picking merge commits, the mainline will always be the first parent. If you want to use a different mainline then you need to do that diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md new file mode 100644 index 00000000000..085170d9f03 --- /dev/null +++ b/doc/user/project/merge_requests/fast_forward_merge.md @@ -0,0 +1,35 @@ +# Fast-forward merge requests + +Retain a linear Git history and a way to accept merge requests without +creating merge commits. + +## Overview + +When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge +commits will be created and all merges are fast-forwarded, which means that +merging is only allowed if the branch could be fast-forwarded. + +When a fast-forward merge is not possible, the user must rebase the branch manually. + +## Use cases + +Sometimes, a workflow policy might mandate a clean commit history without +merge commits. In such cases, the fast-forward merge is the perfect candidate. + +## Enabling fast-forward merges + +1. Navigate to your project's **Settings** and search for the 'Merge method' +1. Select the **Fast-forward merge** option +1. Hit **Save changes** for the changes to take effect + +Now, when you visit the merge request page, you will be able to accept it +**only if a fast-forward merge is possible**. + +![Fast forward merge request](img/ff_merge_mr.png) + +If the target branch is ahead of the source branch, you need to rebase the +source branch locally before you will be able to do a fast-forward merge. + +![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png) + +[ffonly]: https://git-scm.com/docs/git-merge#git-merge---ff-only diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png Binary files differindex 5ab094ab367..7dc344f8cf6 100644 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png Binary files differdeleted file mode 100644 index 42dcb9203ec..00000000000 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png Binary files differindex 71227747182..811b0998f85 100644 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png Binary files differdeleted file mode 100644 index 604eb22f51c..00000000000 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png Binary files differdeleted file mode 100644 index e612a39716e..00000000000 --- a/doc/user/project/merge_requests/img/commit_compare.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png Binary files differnew file mode 100644 index 00000000000..241cc990343 --- /dev/null +++ b/doc/user/project/merge_requests/img/ff_merge_mr.png diff --git a/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png b/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png Binary files differnew file mode 100644 index 00000000000..fb412296efc --- /dev/null +++ b/doc/user/project/merge_requests/img/ff_merge_rebase_locally.png diff --git a/doc/user/project/merge_requests/img/merge_request.png b/doc/user/project/merge_requests/img/merge_request.png Binary files differnew file mode 100644 index 00000000000..f9ca6348953 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_request.png diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png Binary files differindex 33f5a4a7a02..d7f0535d3c5 100644 --- a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png Binary files differdeleted file mode 100644 index ef7b6dae553..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png Binary files differdeleted file mode 100644 index f6540c9dd33..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png Binary files differindex 33c58d2abff..3883fb4bc1c 100644 --- a/doc/user/project/merge_requests/img/versions.png +++ b/doc/user/project/merge_requests/img/versions.png diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png Binary files differindex db978ea7b1d..f5bd85dc7c1 100644 --- a/doc/user/project/merge_requests/img/versions_compare.png +++ b/doc/user/project/merge_requests/img/versions_compare.png diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png Binary files differindex 889a2d93e6c..cc70a5bf14b 100644 --- a/doc/user/project/merge_requests/img/versions_dropdown.png +++ b/doc/user/project/merge_requests/img/versions_dropdown.png diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png Binary files differindex 047b0b4620f..0c492aca363 100644 --- a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png +++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png Binary files differindex 8bd206bc24a..e405879b28a 100644 --- a/doc/user/project/merge_requests/img/wip_mark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png Binary files differindex c0bfa6a35a2..d7f8c419945 100644 --- a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 26c6277d33a..6289fcf3c2b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -3,6 +3,8 @@ Merge requests allow you to exchange changes you made to source code and collaborate with other people on the same project. +![Merge request view](img/merge_request.png) + ## Overview A Merge Request (**MR**) is the basis of GitLab as a code collaboration @@ -23,12 +25,14 @@ With GitLab merge requests, you can: - Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md) - Add a time estimation and the time spent with that merge request with [Time Tracking](../../../workflow/time_tracking.html#time-tracking) - [Resolve merge conflicts from the UI](#resolve-conflicts) +- Enable [fast-forward merge requests](#fast-forward-merge-requests) +- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch + With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter) -- Enable [fast-forward merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html) (available in GitLab Enterprise Edition Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter) - Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter) - Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter) @@ -89,6 +93,22 @@ in a merged merge requests or a commit. [Learn more about cherry-picking changes.](cherry_pick_changes.md) +## Semi-linear history merge requests + +A merge commit is created for every merge, but the branch is only merged if +a fast-forward merge is possible. This ensures that if the merge request build +succeeded, the target branch build will also succeed after merging. + +Navigate to a project's settings, select the **Merge commit with semi-linear +history** option under **Merge Requests: Merge method** and save your changes. + +## Fast-forward merge requests + +If you prefer a linear Git history and a way to accept merge requests without +creating merge commits, you can configure this on a per-project basis. + +[Read more about fast-forward merge requests.](fast_forward_merge.md) + ## Merge when pipeline succeeds When reviewing a merge request that looks ready to merge but still has one or @@ -254,4 +274,4 @@ git checkout origin/merge-requests/1 ``` [protected branches]: ../protected_branches.md -[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition"
\ No newline at end of file +[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition" diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md index 5ead9f4177f..8cf8a59dbfe 100644 --- a/doc/user/project/merge_requests/revert_changes.md +++ b/doc/user/project/merge_requests/revert_changes.md @@ -2,51 +2,39 @@ > [Introduced][ce-1990] in GitLab 8.5. ---- - GitLab implements Git's powerful feature to [revert any commit][git-revert] -with introducing a **Revert** button in Merge Requests and commit details. +with introducing a **Revert** button in merge requests and commit details. ## Reverting a Merge Request -_**Note:** The **Revert** button will only be available for Merge Requests -created since GitLab 8.5. However, you can still revert a Merge Request -by reverting the merge commit from the list of Commits page._ +NOTE: **Note:** +The **Revert** button will only be available for merge requests +created since GitLab 8.5. However, you can still revert a merge request +by reverting the merge commit from the list of Commits page. After the Merge Request has been merged, a **Revert** button will be available -to revert the changes introduced by that Merge Request: - -![Revert Merge Request](img/revert_changes_mr.png) - ---- - -You can revert the changes directly into the selected branch or you can opt to -create a new Merge Request with the revert changes: +to revert the changes introduced by that merge request. -![Revert Merge Request modal](img/revert_changes_mr_modal.png) +![Revert Merge Request](img/cherry_pick_changes_mr.png) ---- +After you click that button, a modal will appear where you can choose to +revert the changes directly into the selected branch or you can opt to +create a new merge request with the revert changes. -After the Merge Request has been reverted, the **Revert** button will not be +After the merge request has been reverted, the **Revert** button will not be available anymore. ## Reverting a Commit You can revert a Commit from the Commit details page: -![Revert commit](img/revert_changes_commit.png) - ---- - -Similar to reverting a Merge Request, you can opt to revert the changes -directly into the target branch or create a new Merge Request to revert the -changes: - -![Revert commit modal](img/revert_changes_commit_modal.png) +![Revert commit](img/cherry_pick_changes_commit.png) ---- +Similar to reverting a merge request, you can opt to revert the changes +directly into the target branch or create a new merge request to revert the +changes. -After the Commit has been reverted, the **Revert** button will not be available +After the commit has been reverted, the **Revert** button will not be available anymore. Please note that when reverting merge commits, the mainline will always be the diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 22c343dc027..a234a647b77 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -23,7 +23,7 @@ Add an [issue description template](../description_templates.md#description-temp Set up your project's merge request settings: -- Set up the merge request method (merge commit, [fast-forward merge](https://docs.gitlab.com/ee/user/project/merge_requests/fast_forward_merge.html#fast-forward-merge-requests)). _Fast-forward is available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)._ +- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)). - Merge request [description templates](../description_templates.md#description-templates). - Enable [merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#merge-request-approvals), _available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)_. - Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md). @@ -42,3 +42,11 @@ Learn how to [export a project](import_export.md#importing-the-project) in GitLa ### Advanced settings Here you can run housekeeping, archive, rename, transfer, or remove a project. + +#### Archiving a project + +>**Note:** Only Project Owners and Admin users have the permission to archive a project + +It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings. + +An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state. diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 673e08287a3..6b2aba47f54 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -36,6 +36,7 @@ - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md) - [Merge requests versions](../user/project/merge_requests/versions.md) - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md) + - [Fast-forward merge requests](../user/project/merge_requests/fast_forward_merge.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature new file mode 100644 index 00000000000..995e52f9332 --- /dev/null +++ b/features/project/ff_merge_requests.feature @@ -0,0 +1,24 @@ +Feature: Project Ff Merge Requests + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" have "Bug NS-05" open merge request with diffs inside + And merge request "Bug NS-05" is mergeable + + @javascript + Scenario: I do ff-only merge for rebased branch + Given ff merge enabled + And merge request "Bug NS-05" is rebased + When I visit merge request page "Bug NS-05" + Then I should see ff-only merge button + When I accept this merge request + Then I should see merged request + + @javascript + Scenario: I do ff-only merge for merged branch + Given ff merge enabled + And merge request "Bug NS-05" merged target + When I visit merge request page "Bug NS-05" + Then I should see ff-only merge button + When I accept this merge request + Then I should see merged request diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb new file mode 100644 index 00000000000..d68fe71e16e --- /dev/null +++ b/features/steps/project/ff_merge_requests.rb @@ -0,0 +1,65 @@ +class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedProject + include SharedNote + include SharedPaths + include SharedMarkdown + include SharedDiffNote + include SharedUser + include WaitForRequests + + step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do + create(:merge_request_with_diffs, + title: "Bug NS-05", + source_project: project, + target_project: project, + author: project.users.first) + end + + step 'I should see ff-only merge button' do + expect(page).to have_content "Fast-forward merge without a merge commit" + expect(page).to have_button 'Merge' + end + + step 'merge request "Bug NS-05" is mergeable' do + merge_request.mark_as_mergeable + end + + step 'I accept this merge request' do + page.within '.mr-state-widget' do + click_button "Merge" + end + end + + step 'I should see merged request' do + page.within '.status-box' do + expect(page).to have_content "Merged" + wait_for_requests + end + end + + step 'ff merge enabled' do + project = merge_request.target_project + project.merge_requests_ff_only_enabled = true + project.save! + end + + step 'merge request "Bug NS-05" is rebased' do + merge_request.source_branch = 'flatten-dir' + merge_request.target_branch = 'improve/awesome' + merge_request.reload_diff + merge_request.save! + end + + step 'merge request "Bug NS-05" merged target' do + merge_request.source_branch = 'merged-target' + merge_request.target_branch = 'improve/awesome' + merge_request.reload_diff + merge_request.save! + end + + def merge_request + @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") + end +end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 2c59ec5bb06..c872bd6f861 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -232,7 +232,7 @@ module SharedDiffNote end def click_parallel_diff_line(code, line_type) - find(".line_holder.parallel .diff-line-num[id='#{code}']").trigger 'mouseover' + find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover' find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5d45b14f592..7082f31b5b8 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1022,6 +1022,7 @@ module API expose :cache, using: Cache expose :credentials, using: Credentials expose :dependencies, using: Dependency + expose :features end end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index ee73fa91589..9cac303e645 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -1,6 +1,18 @@ module Banzai module Filter class MarkdownFilter < HTML::Pipeline::TextFilter + # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + REDCARPET_OPTIONS = { + fenced_code_blocks: true, + footnotes: true, + lax_spacing: true, + no_intra_emphasis: true, + space_after_headers: true, + strikethrough: true, + superscript: true, + tables: true + }.freeze + def initialize(text, context = nil, result = nil) super text, context, result @text = @text.delete "\r" @@ -13,27 +25,11 @@ module Banzai end def self.renderer - @renderer ||= begin + Thread.current[:banzai_markdown_renderer] ||= begin renderer = Banzai::Renderer::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) + Redcarpet::Markdown.new(renderer, REDCARPET_OPTIONS) end end - - def self.redcarpet_options - # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use - @redcarpet_options ||= { - fenced_code_blocks: true, - footnotes: true, - lax_spacing: true, - no_intra_emphasis: true, - space_after_headers: true, - strikethrough: true, - superscript: true, - tables: true - }.freeze - end - - private_class_method :redcarpet_options end end end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 88b17e12576..d8c8deea628 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -73,8 +73,9 @@ module Banzai return unless node.has_attribute?('href') begin + node['href'] = node['href'].strip uri = Addressable::URI.parse(node['href']) - uri.scheme = uri.scheme.strip.downcase if uri.scheme + uri.scheme = uri.scheme.downcase if uri.scheme node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) rescue Addressable::URI::InvalidURIError diff --git a/lib/gitlab/bare_repository_importer.rb b/lib/gitlab/bare_repository_importer.rb index 9323bfc7fb2..1d98d187805 100644 --- a/lib/gitlab/bare_repository_importer.rb +++ b/lib/gitlab/bare_repository_importer.rb @@ -56,7 +56,8 @@ module Gitlab name: project_path, path: project_path, repository_storage: storage_name, - namespace_id: group&.id + namespace_id: group&.id, + skip_disk_validation: true } project = Projects::CreateService.new(user, project_params).execute diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index ad78ae244b2..088adbdd267 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -155,7 +155,9 @@ module Gitlab stream.each_line do |line| s = StringScanner.new(line) until s.eos? - if s.scan(/\e([@-_])(.*?)([@-~])/) + if s.scan(/section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/) + handle_section(s) + elsif s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) elsif s.scan(/\e(([@-_])(.*?)?)?$/) break @@ -183,6 +185,15 @@ module Gitlab ) end + def handle_section(s) + action = s[1] + timestamp = s[2] + section = s[3] + line = s.matched()[0...-5] # strips \r\033[0K + + @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>} + end + def handle_sequence(s) indicator = s[1] commands = s[2].split ';' diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 096301d300f..ca94b4baa59 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -24,41 +24,13 @@ module Gitlab SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze - class << self - # The maximum size of a diff to display. - def size_limit - if RequestStore.active? - RequestStore['gitlab_git_diff_size_limit'] ||= find_size_limit - else - find_size_limit - end - end - - # The maximum size before a diff is collapsed. - def collapse_limit - if RequestStore.active? - RequestStore['gitlab_git_diff_collapse_limit'] ||= find_collapse_limit - else - find_collapse_limit - end - end + # The maximum size of a diff to display. + SIZE_LIMIT = 100.kilobytes - def find_size_limit - if Feature.enabled?('gitlab_git_diff_size_limit_increase') - 200.kilobytes - else - 100.kilobytes - end - end - - def find_collapse_limit - if Feature.enabled?('gitlab_git_diff_size_limit_increase') - 100.kilobytes - else - 10.kilobytes - end - end + # The maximum size before a diff is collapsed. + COLLAPSE_LIMIT = 10.kilobytes + class << self def between(repo, head, base, options = {}, *paths) straight = options.delete(:straight) || false @@ -172,7 +144,7 @@ module Gitlab def too_large? if @too_large.nil? - @too_large = @diff.bytesize >= self.class.size_limit + @too_large = @diff.bytesize >= SIZE_LIMIT else @too_large end @@ -190,7 +162,7 @@ module Gitlab def collapsed? return @collapsed if defined?(@collapsed) - @collapsed = !expanded && @diff.bytesize >= self.class.collapse_limit + @collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT end def collapse! @@ -275,14 +247,14 @@ module Gitlab hunk.each_line do |line| size += line.content.bytesize - if size >= self.class.size_limit + if size >= SIZE_LIMIT too_large! return true end end end - if !expanded && size >= self.class.collapse_limit + if !expanded && size >= COLLAPSE_LIMIT collapse! return true end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index 786e2e7e8dc..d835dcca8ba 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -152,13 +152,15 @@ module Gitlab # (and have!) accidentally reset the ref to an earlier state, clobbering # commits. See also https://github.com/libgit2/libgit2/issues/1534. command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - _, status = popen( + + output, status = popen( command, repository.path) do |stdin| stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") end unless status.zero? + Gitlab::GitLogger.error("'git update-ref' in #{repository.path}: #{output}") raise Gitlab::Git::CommitError.new( "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ " Please refresh and try again.") diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 22b735c6f7b..89b654253cb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -53,14 +53,15 @@ module Gitlab # Rugged repo object attr_reader :rugged - attr_reader :storage, :gl_repository, :relative_path + attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver - # 'path' must be the path to a _bare_ git repository, e.g. - # /path/to/my-repo.git + # This initializer method is only used on the client side (gitlab-ce). + # Gitaly-ruby uses a different initializer. def initialize(storage, relative_path, gl_repository) @storage = storage @relative_path = relative_path @gl_repository = gl_repository + @gitaly_resolver = Gitlab::GitalyClient storage_path = Gitlab.config.repositories.storages[@storage]['path'] @path = File.join(storage_path, @relative_path) @@ -676,7 +677,13 @@ module Gitlab end def rm_branch(branch_name, user:) - OperationService.new(user, self).rm_branch(find_branch(branch_name)) + gitaly_migrate(:operation_user_delete_branch) do |is_enabled| + if is_enabled + gitaly_operations_client.user_delete_branch(branch_name, user) + else + OperationService.new(user, self).rm_branch(find_branch(branch_name)) + end + end end def rm_tag(tag_name, user:) @@ -981,9 +988,9 @@ module Gitlab def with_repo_tmp_commit(start_repository, start_branch_name, sha) tmp_ref = fetch_ref( - start_repository.path, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" + start_repository, + source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + target_ref: "refs/tmp/#{SecureRandom.hex}/head" ) yield commit(sha) @@ -1015,13 +1022,27 @@ module Gitlab end end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) + def write_ref(ref_path, ref) + raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') + raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") + + command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z] + input = "update #{ref_path}\x00#{ref}\x00\x00" + output, status = circuit_breaker.perform do + popen(command, path) { |stdin| stdin.write(input) } + end + + raise GitError, output unless status.zero? end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) + def fetch_ref(source_repository, source_ref:, target_ref:) + message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled| + if is_enabled + gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref) + else + local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref) + end + end # Make sure ref was created, and raise Rugged::ReferenceError when not raise Rugged::ReferenceError, message if status != 0 @@ -1030,9 +1051,9 @@ module Gitlab end # Refactoring aid; allows us to copy code from app/models/repository.rb - def run_git(args) + def run_git(args, env: {}) circuit_breaker.perform do - popen([Gitlab.config.git.bin_path, *args], path) + popen([Gitlab.config.git.bin_path, *args], path, env) end end @@ -1489,9 +1510,33 @@ module Gitlab OperationService.new(user, self).add_branch(branch_name, target_object.oid) find_branch(branch_name) - rescue Rugged::ReferenceError + rescue Rugged::ReferenceError => ex raise InvalidRef, ex end + + def local_fetch_ref(source_path, source_ref:, target_ref:) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + run_git(args) + end + + def gitaly_fetch_ref(source_repository, source_ref:, target_ref:) + gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) + gitaly_address = gitaly_resolver.address(source_repository.storage) + gitaly_token = gitaly_resolver.token(source_repository.storage) + + request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository) + env = { + 'GITALY_ADDRESS' => gitaly_address, + 'GITALY_PAYLOAD' => request.to_json, + 'GITALY_WD' => Dir.pwd, + 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack" + } + env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present? + + args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref}) + + run_git(args, env: env) + end end end end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index e0943d3a3eb..92a6a672534 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -31,7 +31,7 @@ module Gitlab output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) unless status.zero? - raise "Got a non-zero exit code while calling out `#{args.join(' ')}`." + raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}" end output.split("\n") diff --git a/lib/gitlab/git/user.rb b/lib/gitlab/git/user.rb index cb1af5f3b7c..da74719ae87 100644 --- a/lib/gitlab/git/user.rb +++ b/lib/gitlab/git/user.rb @@ -7,6 +7,11 @@ module Gitlab new(gitlab_user.username, gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user)) end + # TODO support the username field in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/628 + def self.from_gitaly(gitaly_user) + new('', gitaly_user.name, gitaly_user.email, gitaly_user.gl_id) + end + def initialize(username, name, email, gl_id) @username = username @name = name diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb new file mode 100644 index 00000000000..d651c931a38 --- /dev/null +++ b/lib/gitlab/git/wiki.rb @@ -0,0 +1,115 @@ +module Gitlab + module Git + class Wiki + DuplicatePageError = Class.new(StandardError) + + CommitDetails = Struct.new(:name, :email, :message) do + def to_h + { name: name, email: email, message: message } + end + end + + def self.default_ref + 'master' + end + + # Initialize with a Gitlab::Git::Repository instance + def initialize(repository) + @repository = repository + end + + def repository_exists? + @repository.exists? + end + + def write_page(name, format, content, commit_details) + assert_type!(format, Symbol) + assert_type!(commit_details, CommitDetails) + + gollum_wiki.write_page(name, format, content, commit_details.to_h) + + nil + rescue Gollum::DuplicatePageError => e + raise Gitlab::Git::Wiki::DuplicatePageError, e.message + end + + def delete_page(page_path, commit_details) + assert_type!(commit_details, CommitDetails) + + gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h) + nil + end + + def update_page(page_path, title, format, content, commit_details) + assert_type!(format, Symbol) + assert_type!(commit_details, CommitDetails) + + gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h) + nil + end + + def pages + gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) } + end + + def page(title:, version: nil, dir: nil) + if version + version = Gitlab::Git::Commit.find(@repository, version).id + end + + gollum_page = gollum_wiki.page(title, version, dir) + return unless gollum_page + + new_page(gollum_page) + end + + def file(name, version) + version ||= self.class.default_ref + gollum_file = gollum_wiki.file(name, version) + return unless gollum_file + + Gitlab::Git::WikiFile.new(gollum_file) + end + + def page_versions(page_path) + current_page = gollum_page_by_path(page_path) + current_page.versions.map do |gollum_git_commit| + gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id) + new_version(gollum_page, gollum_git_commit.id) + end + end + + def preview_slug(title, format) + gollum_wiki.preview_page(title, '', format).url_path + end + + private + + def gollum_wiki + @gollum_wiki ||= Gollum::Wiki.new(@repository.path) + end + + def gollum_page_by_path(page_path) + page_name = Gollum::Page.canonicalize_filename(page_path) + page_dir = File.split(page_path).first + + gollum_wiki.paged(page_name, page_dir) + end + + def new_page(gollum_page) + Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id)) + end + + def new_version(gollum_page, commit_id) + commit = Gitlab::Git::Commit.find(@repository, commit_id) + Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format) + end + + def assert_type!(object, klass) + unless object.is_a?(klass) + raise ArgumentError, "expected a #{klass}, got #{object.inspect}" + end + end + end + end +end diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb new file mode 100644 index 00000000000..527f2a44dea --- /dev/null +++ b/lib/gitlab/git/wiki_file.rb @@ -0,0 +1,19 @@ +module Gitlab + module Git + class WikiFile + attr_reader :mime_type, :raw_data, :name + + # This class is meant to be serializable so that it can be constructed + # by Gitaly and sent over the network to GitLab. + # + # Because Gollum::File is not serializable we must get all the data from + # 'gollum_file' during initialization, and NOT store it in an instance + # variable. + def initialize(gollum_file) + @mime_type = gollum_file.mime_type + @raw_data = gollum_file.raw_data + @name = gollum_file.name + end + end + end +end diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb new file mode 100644 index 00000000000..a06bac4414f --- /dev/null +++ b/lib/gitlab/git/wiki_page.rb @@ -0,0 +1,39 @@ +module Gitlab + module Git + class WikiPage + attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical + + # This class is meant to be serializable so that it can be constructed + # by Gitaly and sent over the network to GitLab. + # + # Because Gollum::Page is not serializable we must get all the data from + # 'gollum_page' during initialization, and NOT store it in an instance + # variable. + # + # Note that 'version' is a WikiPageVersion instance which it itself + # serializable. That means it's OK to store 'version' in an instance + # variable. + def initialize(gollum_page, version) + @url_path = gollum_page.url_path + @title = gollum_page.title + @format = gollum_page.format + @path = gollum_page.path + @raw_data = gollum_page.raw_data + @name = gollum_page.name + @historical = gollum_page.historical? + + @version = version + end + + def historical? + @historical + end + + def text_data + return @text_data if defined?(@text_data) + + @text_data = @raw_data && Gitlab::EncodingHelper.encode!(@raw_data.dup) + end + end + end +end diff --git a/lib/gitlab/git/wiki_page_version.rb b/lib/gitlab/git/wiki_page_version.rb new file mode 100644 index 00000000000..55f1afedcab --- /dev/null +++ b/lib/gitlab/git/wiki_page_version.rb @@ -0,0 +1,19 @@ +module Gitlab + module Git + class WikiPageVersion + attr_reader :commit, :format + + # This class is meant to be serializable so that it can be constructed + # by Gitaly and sent over the network to GitLab. + # + # Both 'commit' (a Gitlab::Git::Commit) and 'format' (a string) are + # serializable. + def initialize(commit, format) + @commit = commit + @format = format + end + + delegate :message, :sha, :id, :author_name, :authored_date, to: :commit + end + end +end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index e75e0500ed8..87b300dcf7e 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -233,6 +233,8 @@ module Gitlab end def self.encode(s) + return "" if s.nil? + s.dup.force_encoding(Encoding::ASCII_8BIT) end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 36da63fd586..a2b50f2507e 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -274,7 +274,7 @@ module Gitlab repository: @gitaly_repo, left_commit_id: from_id, right_commit_id: to_id, - paths: options.fetch(:paths, []).map { |path| GitalyClient.encode(path) } + paths: options.fetch(:paths, []).compact.map { |path| GitalyClient.encode(path) } } end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 46bd5c18603..81ddaf13e10 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -60,6 +60,20 @@ module Gitlab target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit) end + + def user_delete_branch(branch_name, user) + request = Gitaly::UserDeleteBranchRequest.new( + repository: @gitaly_repo, + branch_name: GitalyClient.encode(branch_name), + user: Util.gitaly_user(user) + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + end + end end end end diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index cdbdfa10d0e..da43bd0af4b 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -113,7 +113,7 @@ module Gitlab def kubeconfig_embed_ca_pem(config, ca_pem) cluster = config.dig(:clusters, 0, :cluster) - cluster[:'certificate-authority-data'] = Base64.encode64(ca_pem) + cluster[:'certificate-authority-data'] = Base64.strict_encode64(ca_pem) end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index cd7e4ca7b7e..0afaa2306b5 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -22,8 +22,8 @@ module Gitlab Gitlab::LDAP::Config.new(provider) end - def users(field, value, limit = nil) - options = user_options(field, value, limit) + def users(fields, value, limit = nil) + options = user_options(Array(fields), value, limit) entries = ldap_search(options).select do |entry| entry.respond_to? config.uid @@ -72,20 +72,24 @@ module Gitlab private - def user_options(field, value, limit) - options = { attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq } + def user_options(fields, value, limit) + options = { + attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq, + base: config.base + } + options[:size] = limit if limit - if field.to_sym == :dn + if fields.include?('dn') + raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1 + options[:base] = value options[:scope] = Net::LDAP::SearchScope_BaseObject - options[:filter] = user_filter else - options[:base] = config.base - options[:filter] = user_filter(Net::LDAP::Filter.eq(field, value)) + filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|) end - options + options.merge(filter: user_filter(filter)) end def user_filter(filter = nil) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 4d6f8ac79de..9a6f7827b16 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -17,6 +17,12 @@ module Gitlab adapter.user('dn', dn) end + def self.find_by_email(email, adapter) + email_fields = adapter.config.attributes['email'] + + adapter.user(email_fields, email) + end + def self.disabled_via_active_directory?(dn, adapter) adapter.dn_matches_filter?(dn, AD_USER_DISABLED) end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 3bf27b37ae6..1793097363e 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -17,41 +17,19 @@ module Gitlab end end - def initialize(auth_hash) - super - update_user_attributes - end - def save super('LDAP') end # instance methods - def gl_user - @gl_user ||= find_by_uid_and_provider || find_by_email || build_new_user + def find_user + find_by_uid_and_provider || find_by_email || build_new_user end def find_by_uid_and_provider self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider) end - def find_by_email - ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_attribute?(:email) - end - - def update_user_attributes - if persisted? - # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved. - identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider } - identity ||= gl_user.identities.build(provider: auth_hash.provider) - - # For a new identity set extern_uid to the LDAP DN - # For an existing identity with matching email but changed DN, update the DN. - # For an existing identity with no change in DN, this line changes nothing. - identity.extern_uid = auth_hash.uid - end - end - def changed? gl_user.changed? || gl_user.identities.any?(&:changed?) end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index e06d4dc45f7..68815be4d13 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -13,6 +13,7 @@ module Gitlab def initialize(auth_hash) self.auth_hash = auth_hash update_profile if sync_profile_from_provider? + add_or_update_user_identities end def persisted? @@ -44,47 +45,54 @@ module Gitlab end def gl_user - @user ||= find_by_uid_and_provider + return @gl_user if defined?(@gl_user) - if auto_link_ldap_user? - @user ||= find_or_create_ldap_user - end + @gl_user = find_user + end - if signup_enabled? - @user ||= build_new_user - end + def find_user + user = find_by_uid_and_provider - if external_provider? && @user - @user.external = true - end + user ||= find_or_build_ldap_user if auto_link_ldap_user? + user ||= build_new_user if signup_enabled? + + user.external = true if external_provider? && user - @user + user end protected - def find_or_create_ldap_user + def add_or_update_user_identities + # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved. + identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider } + + identity ||= gl_user.identities.build(provider: auth_hash.provider) + identity.extern_uid = auth_hash.uid + + if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person + log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}." + gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn) + end + end + + def find_or_build_ldap_user return unless ldap_person - # If a corresponding person exists with same uid in a LDAP server, - # check if the user already has a GitLab account. user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) if user - # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account. log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity." - user.identities.find_or_initialize_by(extern_uid: auth_hash.uid, provider: auth_hash.provider) - else - log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account." - user = find_by_uid_and_provider - if user.nil? - log.info "No user found using #{auth_hash.provider} provider. Creating a new one." - user = build_new_user - end - log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}." - user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) + return user end - user + log.info "No user found using #{auth_hash.provider} provider. Creating a new one." + build_new_user + end + + def find_by_email + return unless auth_hash.has_attribute?(:email) + + ::User.find_by(email: auth_hash.email.downcase) end def auto_link_ldap_user? @@ -108,9 +116,9 @@ module Gitlab end def find_ldap_person(auth_hash, adapter) - by_uid = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) - # The `uid` might actually be a DN. Try it next. - by_uid || Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter) + Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) || + Gitlab::LDAP::Person.find_by_email(auth_hash.uid, adapter) || + Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter) end def ldap_config @@ -152,7 +160,7 @@ module Gitlab end def build_new_user - user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true) + user_params = user_attributes.merge(skip_confirmation: true) Users::BuildService.new(nil, user_params).execute(skip_authorization: true) end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index 0f323a9e8b2..e0a9d1dee77 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -10,41 +10,20 @@ module Gitlab super('SAML') end - def gl_user - if auto_link_ldap_user? - @user ||= find_or_create_ldap_user - end - - @user ||= find_by_uid_and_provider - - if auto_link_saml_user? - @user ||= find_by_email - end + def find_user + user = find_by_uid_and_provider - if signup_enabled? - @user ||= build_new_user - end + user ||= find_by_email if auto_link_saml_user? + user ||= find_or_build_ldap_user if auto_link_ldap_user? + user ||= build_new_user if signup_enabled? - if external_users_enabled? && @user + if external_users_enabled? && user # Check if there is overlap between the user's groups and the external groups # setting then set user as external or internal. - @user.external = - if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? - false - else - true - end + user.external = !(auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? end - @user - end - - def find_by_email - if auth_hash.has_attribute?(:email) - user = ::User.find_by(email: auth_hash.email.downcase) - user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider) if user - user - end + user end def changed? diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 222021e8802..f30c771837a 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -12,8 +12,9 @@ module Gitlab # # Project.where("id IN (#{sql})") class Union - def initialize(relations) + def initialize(relations, remove_duplicates: true) @relations = relations + @remove_duplicates = remove_duplicates end def to_sql @@ -25,7 +26,11 @@ module Gitlab @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end - fragments.join("\nUNION\n") + fragments.join("\n#{union_keyword}\n") + end + + def union_keyword + @remove_duplicates ? 'UNION' : 'UNION ALL' end end end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 4e1ec1402ea..1caa791c1be 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -1,7 +1,9 @@ module Gitlab class UrlSanitizer + ALLOWED_SCHEMES = %w[http https ssh git].freeze + def self.sanitize(content) - regexp = URI::Parser.new.make_regexp(%w(http https ssh git)) + regexp = URI::Parser.new.make_regexp(ALLOWED_SCHEMES) content.gsub(regexp) { |url| new(url).masked_url } rescue Addressable::URI::InvalidURIError @@ -11,9 +13,9 @@ module Gitlab def self.valid?(url) return false unless url.present? - Addressable::URI.parse(url.strip) + uri = Addressable::URI.parse(url.strip) - true + ALLOWED_SCHEMES.include?(uri.scheme) rescue Addressable::URI::InvalidURIError false end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 45f246242f1..f200c694562 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -89,6 +89,13 @@ module Gitlab params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) raise "Repository or ref not found" if params.empty? + if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive) + params.merge!( + 'GitalyServer' => gitaly_server_hash(repository), + 'GitalyRepository' => repository.gitaly_repository.to_h + ) + end + [ SEND_DATA_HEADER, "git-archive:#{encode(params)}" diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb index 7b486d78cf0..dfa8b8b3f5b 100644 --- a/lib/system_check/app/git_user_default_ssh_config_check.rb +++ b/lib/system_check/app/git_user_default_ssh_config_check.rb @@ -5,6 +5,7 @@ module SystemCheck # whitelisted as it may change the SSH client's behaviour dramatically. WHITELIST = %w[ authorized_keys + authorized_keys.lock authorized_keys2 known_hosts ].freeze diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 259a755d724..a42f02a84fd 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -3,8 +3,8 @@ namespace :gitlab do desc 'GitLab | Assets | Compile all frontend assets' task compile: [ 'yarn:check', - 'rake:assets:precompile', 'gettext:po_to_json', + 'rake:assets:precompile', 'webpack:compile', 'fix_urls' ] diff --git a/qa/Gemfile b/qa/Gemfile index 5d089a45934..ff29824529f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' +gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'capybara', '~> 2.12.1' gem 'capybara-screenshot', '~> 1.0.14' gem 'rake', '~> 12.0.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 4dd71aa5010..95aeef10752 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -3,6 +3,7 @@ GEM specs: addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) + byebug (9.0.6) capybara (2.12.1) addressable mime-types (>= 1.16) @@ -13,22 +14,27 @@ GEM capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - capybara-webkit (1.12.0) - capybara (>= 2.3.0, < 2.13.0) - json childprocess (0.7.0) ffi (~> 1.0, >= 1.0.11) + coderay (1.1.1) diff-lcs (1.3) ffi (1.9.18) - json (2.0.3) launchy (2.4.3) addressable (~> 2.3) + method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) + pry (~> 0.10) public_suffix (2.0.5) rack (2.0.1) rack-test (0.6.3) @@ -52,6 +58,7 @@ GEM childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) + slop (3.6.0) websocket (1.2.4) xpath (2.0.0) nokogiri (~> 1.3) @@ -62,7 +69,7 @@ PLATFORMS DEPENDENCIES capybara (~> 2.12.1) capybara-screenshot (~> 1.0.14) - capybara-webkit (~> 1.12.0) + pry-byebug (~> 3.4.1) rake (~> 12.0.0) rspec (~> 3.5) selenium-webdriver (~> 2.53) diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index f4619042e34..baa06b1c75e 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -4,8 +4,6 @@ module QA class Menu < Page::Base def go_to_license link = find_link 'License' - # Click space to scroll this link into the view - link.send_keys(:space) link.click end end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 56a270d8fcc..68d9597c4d2 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -5,8 +5,8 @@ module QA def choose_repository_clone_http find('#clone-dropdown').click - page.within('#clone-dropdown') do - find('span', text: 'HTTP').click + page.within('.clone-options-dropdown') do + click_link('HTTP') end end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 4dfdd6cd93c..79c681168cc 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -43,8 +43,7 @@ module QA Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { - 'binary' => '/usr/bin/google-chrome-stable', - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024] + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] } ) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index b4a22a46b51..053bd73fee3 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -207,162 +207,6 @@ describe Projects::IssuesController do end end - describe 'PUT #update' do - before do - sign_in(user) - project.team << [user, :developer] - end - - it_behaves_like 'update invalid issuable', Issue - - context 'changing the assignee' do - it 'limits the attributes exposed on the assignee' do - assignee = create(:user) - project.add_developer(assignee) - - put :update, - namespace_id: project.namespace.to_param, - project_id: project, - id: issue.iid, - issue: { assignee_ids: [assignee.id] }, - format: :json - body = JSON.parse(response.body) - - expect(body['assignees'].first.keys) - .to match_array(%w(id name username avatar_url state web_url)) - end - end - - context 'Akismet is enabled' do - let(:project) { create(:project_empty_repo, :public) } - - before do - stub_application_setting(recaptcha_enabled: true) - allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) - end - - context 'when an issue is not identified as spam' do - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false) - end - - it 'normally updates the issue' do - expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo') - end - end - - context 'when an issue is identified as spam' do - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) - end - - context 'when captcha is not verified' do - def update_spam_issue - update_issue(title: 'Spam Title', description: 'Spam lives here') - end - - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) - end - - it 'rejects an issue recognized as a spam' do - expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) - expect { update_spam_issue }.not_to change { issue.reload.title } - end - - it 'rejects an issue recognized as a spam when recaptcha disabled' do - stub_application_setting(recaptcha_enabled: false) - - expect { update_spam_issue }.not_to change { issue.reload.title } - end - - it 'creates a spam log' do - update_spam_issue - - spam_logs = SpamLog.all - - expect(spam_logs.count).to eq(1) - expect(spam_logs.first.title).to eq('Spam Title') - expect(spam_logs.first.recaptcha_verified).to be_falsey - end - - context 'as HTML' do - it 'renders verify template' do - update_spam_issue - - expect(response).to render_template(:verify) - end - end - - context 'as JSON' do - before do - update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json) - end - - it 'renders json errors' do - expect(json_response) - .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) - end - - it 'returns 422 status' do - expect(response).to have_http_status(422) - end - end - end - - context 'when captcha is verified' do - let(:spammy_title) { 'Whatever' } - let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) } - - def update_verified_issue - update_issue({ title: spammy_title }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - end - - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha) - .and_return(true) - end - - it 'redirect to issue page' do - update_verified_issue - - expect(response) - .to redirect_to(project_issue_path(project, issue)) - end - - it 'accepts an issue after recaptcha is verified' do - expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title) - end - - it 'marks spam log as recaptcha_verified' do - expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) - end - - it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do - spam_log = create(:spam_log) - - expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) } - .not_to change { SpamLog.last.recaptcha_verified } - end - end - end - - def update_issue(issue_params = {}, additional_params = {}) - params = { - namespace_id: project.namespace.to_param, - project_id: project, - id: issue.iid, - issue: issue_params - }.merge(additional_params) - - put :update, params - end - end - end - describe 'POST #move' do before do sign_in(user) @@ -533,6 +377,146 @@ describe Projects::IssuesController do end end + describe 'PUT #update' do + def update_issue(issue_params: {}, additional_params: {}, id: nil) + id ||= issue.iid + params = { + namespace_id: project.namespace.to_param, + project_id: project, + id: id, + issue: { title: 'New title' }.merge(issue_params), + format: :json + }.merge(additional_params) + + put :update, params + end + + def go(id:) + update_issue(id: id) + end + + before do + sign_in(user) + project.team << [user, :developer] + end + + it_behaves_like 'restricted action', success: 200 + it_behaves_like 'update invalid issuable', Issue + + context 'changing the assignee' do + it 'limits the attributes exposed on the assignee' do + assignee = create(:user) + project.add_developer(assignee) + + update_issue(issue_params: { assignee_ids: [assignee.id] }) + + body = JSON.parse(response.body) + + expect(body['assignees'].first.keys) + .to match_array(%w(id name username avatar_url state web_url)) + end + end + + context 'Akismet is enabled' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + stub_application_setting(recaptcha_enabled: true) + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + end + + context 'when an issue is not identified as spam' do + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false) + end + + it 'normally updates the issue' do + expect { update_issue(issue_params: { title: 'Foo' }) }.to change { issue.reload.title }.to('Foo') + end + end + + context 'when an issue is identified as spam' do + before do + allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) + end + + context 'when captcha is not verified' do + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + end + + it 'rejects an issue recognized as a spam' do + expect { update_issue }.not_to change { issue.reload.title } + end + + it 'rejects an issue recognized as a spam when recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) + + expect { update_issue }.not_to change { issue.reload.title } + end + + it 'creates a spam log' do + update_issue(issue_params: { title: 'Spam title' }) + + spam_logs = SpamLog.all + + expect(spam_logs.count).to eq(1) + expect(spam_logs.first.title).to eq('Spam title') + expect(spam_logs.first.recaptcha_verified).to be_falsey + end + + it 'renders json errors' do + update_issue + + expect(json_response) + .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) + end + + it 'returns 422 status' do + update_issue + + expect(response).to have_http_status(422) + end + end + + context 'when captcha is verified' do + let(:spammy_title) { 'Whatever' } + let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) } + + def update_verified_issue + update_issue( + issue_params: { title: spammy_title }, + additional_params: { spam_log_id: spam_logs.last.id, recaptcha_verification: true }) + end + + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha) + .and_return(true) + end + + it 'returns 200 status' do + expect(response).to have_http_status(200) + end + + it 'accepts an issue after recaptcha is verified' do + expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title) + end + + it 'marks spam log as recaptcha_verified' do + expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) + end + + it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do + spam_log = create(:spam_log) + + expect { update_issue(issue_params: { spam_log_id: spam_log.id, recaptcha_verification: true }) } + .not_to change { SpamLog.last.recaptcha_verified } + end + end + end + end + end + describe 'GET #show' do it_behaves_like 'restricted action', success: 200 @@ -573,29 +557,6 @@ describe Projects::IssuesController do end end end - - describe 'GET #edit' do - it_behaves_like 'restricted action', success: 200 - - def go(id:) - get :edit, - namespace_id: project.namespace.to_param, - project_id: project, - id: id - end - end - - describe 'PUT #update' do - it_behaves_like 'restricted action', success: 302 - - def go(id:) - put :update, - namespace_id: project.namespace.to_param, - project_id: project, - id: id, - issue: { title: 'New title' } - end - end end describe 'POST #create' do diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index fdd7e6f173f..d01339a0b88 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -216,7 +216,7 @@ describe Projects::JobsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6775012bab5..e46d1995498 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do expect(response).to match_response_schema('entities/merge_request') end end - - context 'number of queries', :request_store do - it 'verifies number of queries' do - # pre-create objects - merge_request - - recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } - - expect(recorded.count).to be_within(5).of(30) - expect(recorded.cached_count).to eq(0) - end - end end describe "as diff" do @@ -658,7 +646,7 @@ describe Projects::MergeRequestsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 6ffe41b8608..c0337f96fc6 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -120,6 +120,40 @@ describe Projects::NotesController do expect(note_json[:diff_discussion_html]).to be_nil end end + + context 'with cross-reference system note', :request_store do + let(:new_issue) { create(:issue) } + let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" } + + before do + note + create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference) + end + + it 'filters notes that the user should not see' do + get :index, request_params + + expect(parsed_response[:notes].count).to eq(1) + expect(note_json[:id]).to eq(note.id) + end + + it 'does not result in N+1 queries' do + # Instantiate the controller variables to ensure QueryRecorder has an accurate base count + get :index, request_params + + RequestStore.clear! + + control_count = ActiveRecord::QueryRecorder.new do + get :index, request_params + end.count + + RequestStore.clear! + + create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference) + + expect { get :index, request_params }.not_to exceed_query_limit(control_count) + end + end end describe 'POST create' do diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index f9d77c7ad03..167e80ed9cd 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -142,7 +142,7 @@ describe Projects::PipelinesController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to eq "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") end end diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb index 2805968dcd9..5d9d5351687 100644 --- a/spec/controllers/projects/registry/repositories_controller_spec.rb +++ b/spec/controllers/projects/registry/repositories_controller_spec.rb @@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do expect { go_to_index }.to change { ContainerRepository.all.count }.by(1) expect(ContainerRepository.first).to be_root_repository end + + it 'json has a list of projects' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end end context 'when there are no tags for this repository' do @@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do it 'does not ensure root container repository' do expect { go_to_index }.not_to change { ContainerRepository.all.count } end + + it 'responds with json if asked' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(json_response).to be_kind_of(Array) + end + end + end + end + + describe 'DELETE destroy' do + context 'when root container repository exists' do + let!(:repository) do + create(:container_repository, :root, project: project) + end + + before do + stub_container_registry_tags(repository: :any, tags: []) + end + + it 'deletes a repository' do + expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1) + + expect(response).to have_http_status(:no_content) end end end @@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do end end - def go_to_index + def go_to_index(format: :html) get :index, namespace_id: project.namespace, - project_id: project + project_id: project, + format: format + end + + def delete_repository(repository) + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: repository, + format: :json end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index f4af3587d23..bb702ebeb23 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do let(:user) { create(:user) } let(:project) { create(:project, :private) } + let(:repository) do + create(:container_repository, name: 'image', project: project) + end + before do sign_in(user) stub_container_registry_config(enabled: true) end - context 'when user has access to registry' do + describe 'GET index' do + let(:tags) do + Array.new(40) { |i| "tag#{i}" } + end + before do - project.add_developer(user) + stub_container_registry_tags(repository: /image/, tags: tags) end - describe 'POST destroy' do + context 'when user can control the registry' do + before do + project.add_developer(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user can read the registry' do + before do + project.add_reporter(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user does not have access to registry' do + before do + project.add_guest(user) + end + + it 'does not receive a list of tags' do + get_tags + + expect(response).to have_http_status(:not_found) + end + end + + private + + def get_tags + get :index, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + format: :json + end + end + + describe 'POST destroy' do + context 'when user has access to registry' do + before do + project.add_developer(user) + end + context 'when there is matching tag present' do before do - stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.]) - end - - let(:repository) do - create(:container_repository, name: 'image', project: project) + stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) end it 'makes it possible to delete regular tag' do @@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do end end end - end - def destroy_tag(name) - post :destroy, namespace_id: project.namespace, - project_id: project, - repository_id: repository, - id: name + private + + def destroy_tag(name) + post :destroy, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + id: name, + format: :json + end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4459e227fb3..2a91a6613e6 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -289,6 +289,24 @@ describe ProjectsController do end end + it 'updates Fast Forward Merge attributes' do + controller.instance_variable_set(:@project, project) + + params = { + merge_method: :ff + } + + put :update, + namespace_id: project.namespace, + id: project.id, + project: params + + expect(response).to have_http_status(302) + params.each do |param, value| + expect(project.public_send(param)).to eq(value) + end + end + def update_project(**parameters) put :update, namespace_id: project.namespace.path, diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index e5abfd67d60..0dd1238d6e2 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -12,7 +12,7 @@ FactoryGirl.define do deployment.project ||= deployment.environment.project unless deployment.project.repository_exists? - allow(deployment.project.repository).to receive(:fetch_ref) + allow(deployment.project.repository).to receive(:create_ref) end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index ae39ba4da6b..45213dc6995 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Container Registry" do +describe "Container Registry", js: true do let(:user) { create(:user) } let(:project) { create(:project) } @@ -41,16 +41,19 @@ describe "Container Registry" do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(true) - click_on 'Remove repository' + click_on(class: 'js-remove-repo') end scenario 'user removes a specific tag from container repository' do visit_container_registry + find('.js-toggle-repo').trigger('click') + wait_for_requests + expect_any_instance_of(ContainerRegistry::Tag) .to receive(:delete).and_return(true) - click_on 'Remove tag' + click_on(class: 'js-delete-registry') end end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index dfeba722ac6..ebcd0ba0dcd 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -446,7 +446,7 @@ describe 'Copy as GFM', js: true do def verify(label, *gfms) aggregate_failures(label) do gfms.each do |gfm| - html = gfm_to_html(gfm) + html = gfm_to_html(gfm).gsub(/\A
|
\z/, '') output_gfm = html_to_gfm(html) expect(output_gfm.strip).to eq(gfm.strip) end @@ -463,42 +463,98 @@ describe 'Copy as GFM', js: true do let(:project) { create(:project, :repository) } context 'from a diff' do - before do - visit project_commit_path(project, sample_commit.id) - end + shared_examples 'copying code from a diff' do + context 'selecting one word of text' do + it 'copies as inline code' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no', - context 'selecting one word of text' do - it 'copies as inline code' do - verify( - '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no', + '`RuntimeError`', - '`RuntimeError`' - ) + target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]' + ) + end end - end - context 'selecting one line of text' do - it 'copies as inline code' do - verify( - '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line', + context 'selecting one line of text' do + it 'copies as inline code' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]', - '`raise RuntimeError, "System commands must be given as an array of strings"`' - ) + '`raise RuntimeError, "System commands must be given as an array of strings"`', + + target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]' + ) + end + end + + context 'selecting multiple lines of text' do + it 'copies as a code block' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', + + <<-GFM.strip_heredoc, + ```ruby + raise RuntimeError, "System commands must be given as an array of strings" + end + ``` + GFM + + target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]' + ) + end end end - context 'selecting multiple lines of text' do - it 'copies as a code block' do - verify( - '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', + context 'inline diff' do + before do + visit project_commit_path(project, sample_commit.id, view: 'inline') + end - <<-GFM.strip_heredoc, - ```ruby - raise RuntimeError, "System commands must be given as an array of strings" - end - ``` - GFM - ) + it_behaves_like 'copying code from a diff' + end + + context 'parallel diff' do + before do + visit project_commit_path(project, sample_commit.id, view: 'parallel') + end + + it_behaves_like 'copying code from a diff' + + context 'selecting code on the left' do + it 'copies as a code block' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', + + <<-GFM.strip_heredoc, + ```ruby + unless cmd.is_a?(Array) + raise "System commands must be given as an array of strings" + end + ``` + GFM + + target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].left-side' + ) + end + end + + context 'selecting code on the right' do + it 'copies as a code block' do + verify( + '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', + + <<-GFM.strip_heredoc, + ```ruby + unless cmd.is_a?(Array) + raise RuntimeError, "System commands must be given as an array of strings" + end + ``` + GFM + + target: '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"].right-side' + ) + end end end end @@ -587,9 +643,9 @@ describe 'Copy as GFM', js: true do end end - def verify(selector, gfm) + def verify(selector, gfm, target: nil) html = html_for_selector(selector) - output_gfm = html_to_gfm(html, 'transformCodeSelection') + output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target) expect(output_gfm.strip).to eq(gfm.strip) end end @@ -605,15 +661,21 @@ describe 'Copy as GFM', js: true do page.evaluate_script(js) end - def html_to_gfm(html, transformer = 'transformGFMSelection') + def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil) js = <<-JS.strip_heredoc (function(html) { var transformer = window.gl.CopyAsGFM[#{transformer.inspect}]; var node = document.createElement('div'); - node.innerHTML = html; + $(html).each(function() { node.appendChild(this) }); + + var targetSelector = #{target.to_json}; + var target; + if (targetSelector) { + target = document.querySelector(targetSelector); + } - node = transformer(node); + node = transformer(node, target); if (!node) return null; return window.gl.CopyAsGFM.nodeToGFM(node); diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 2db6f9a2982..8ce470fc288 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -218,54 +218,15 @@ describe 'New/edit issue', :js do context 'edit issue' do before do - visit edit_project_issue_path(project, issue) - end - - it 'allows user to update issue' do - expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) - expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible - - page.within '.js-user-search' do - expect(page).to have_content user.name - end - - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end - - click_button 'Labels' - page.within '.dropdown-menu-labels' do - click_link label.title - click_link label2.title - end - page.within '.js-label-select' do - expect(page).to have_content label.title - end - expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) - expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) - - click_button 'Save changes' - - page.within '.issuable-sidebar' do - page.within '.assignee' do - expect(page).to have_content user.name - end - - page.within '.milestone' do - expect(page).to have_content milestone.title - end - - page.within '.labels' do - expect(page).to have_content label.title - expect(page).to have_content label2.title - end + visit project_issue_path(project, issue) + page.within('.content .issuable-actions') do + click_on 'Edit' end end it 'description has autocomplete' do - find('#issue_description').native.send_keys('') - fill_in 'issue_description', with: '@' + find_field('issue-description').native.send_keys('') + fill_in 'issue-description', with: '@' expect(page).to have_selector('.atwho-view') end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index b4222edbcd0..aa8cf3b013c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Issues' do +describe 'Issues', :js do include DropzoneHelper include IssueHelpers include SortingHelper @@ -24,109 +24,15 @@ describe 'Issues' do end before do - visit edit_project_issue_path(project, issue) - find('.js-zen-enter').click - end - - it 'opens new issue popup' do - expect(page).to have_content("Issue ##{issue.iid}") - end - end - - describe 'Editing issue assignee' do - let!(:issue) do - create(:issue, - author: user, - assignees: [user], - project: project) - end - - it 'allows user to select unassigned', js: true do - visit edit_project_issue_path(project, issue) - - expect(page).to have_content "Assignee #{user.name}" - - first('.js-user-search').click - click_link 'Unassigned' - - click_button 'Save changes' - - page.within('.assignee') do - expect(page).to have_content 'No assignee - assign yourself' - end - - expect(issue.reload.assignees).to be_empty - end - end - - describe 'due date', js: true do - context 'on new form' do - before do - visit new_project_issue_path(project) - end - - it 'saves with due date' do - date = Date.today.at_beginning_of_month - - fill_in 'issue_title', with: 'bug 345' - fill_in 'issue_description', with: 'bug description' - find('#issuable-due-date').click - - page.within '.pika-single' do - click_button date.day - end - - expect(find('#issuable-due-date').value).to eq date.to_s - - click_button 'Submit issue' - - page.within '.issuable-sidebar' do - expect(page).to have_content date.to_s(:medium) - end + visit project_issue_path(project, issue) + page.within('.content .issuable-actions') do + find('.issuable-edit').click end + find('.issue-details .content-block .js-zen-enter').click end - context 'on edit form' do - let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) } - - before do - visit edit_project_issue_path(project, issue) - end - - it 'saves with due date' do - date = Date.today.at_beginning_of_month - - expect(find('#issuable-due-date').value).to eq date.to_s - - date = date.tomorrow - - fill_in 'issue_title', with: 'bug 345' - fill_in 'issue_description', with: 'bug description' - find('#issuable-due-date').click - - page.within '.pika-single' do - click_button date.day - end - - expect(find('#issuable-due-date').value).to eq date.to_s - - click_button 'Save changes' - - page.within '.issuable-sidebar' do - expect(page).to have_content date.to_s(:medium) - end - end - - it 'warns about version conflict' do - issue.update(title: "New title") - - fill_in 'issue_title', with: 'bug 345' - fill_in 'issue_description', with: 'bug description' - - click_button 'Save changes' - - expect(page).to have_content 'Someone edited the issue the same time you did' - end + it 'opens new issue popup' do + expect(page).to have_content(issue.description) end end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 9bcb78d5206..4766cdf716f 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -84,7 +84,7 @@ feature 'Diff note avatars', js: true do end it 'shows note avatar' do - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').click expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) @@ -92,7 +92,7 @@ feature 'Diff note avatars', js: true do end it 'shows comment on note avatar' do - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').click expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") @@ -100,13 +100,13 @@ feature 'Diff note avatars', js: true do end it 'toggles comments when clicking avatar' do - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').click end expect(page).to have_selector('.notes_holder', visible: false) - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do first('img.js-diff-comment-avatar').click end @@ -122,7 +122,7 @@ feature 'Diff note avatars', js: true do wait_for_requests - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do expect(page).not_to have_selector('img.js-diff-comment-avatar') end end @@ -138,7 +138,7 @@ feature 'Diff note avatars', js: true do wait_for_requests end - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) @@ -158,7 +158,7 @@ feature 'Diff note avatars', js: true do end end - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) @@ -176,7 +176,7 @@ feature 'Diff note avatars', js: true do end it 'shows extra comment count' do - page.within find("[id='#{position.line_code(project.repository)}']") do + page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').click expect(find('.diff-comments-more-count')).to have_content '+1' @@ -185,4 +185,10 @@ feature 'Diff note avatars', js: true do end end end + + def find_line(line_code) + line = find("[id='#{line_code}']") + line = line.find(:xpath, 'preceding-sibling::*[1][self::td]') if line.tag_name == 'td' + line + end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 443b596b3c6..791cfa308c3 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -202,6 +202,28 @@ describe 'Merge request', :js do end end + context 'view merge request where fast-forward merge is not possible' do + before do + project.update(merge_requests_ff_only_enabled: true) + + merge_request.update( + merge_user: merge_request.author, + merge_status: :cannot_be_merged + ) + + visit project_merge_request_path(project, merge_request) + end + + it 'shows information about the merge error' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_requests + + page.within('.mr-widget-body') do + expect(page).to have_content('Fast-forward merge is not possible') + end + end + end + context 'merge error' do before do allow_any_instance_of(Repository).to receive(:merge).and_return(false) diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index ad06cee4e81..2f407b13c2f 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do describe 'when checking branches' do context 'with artifacts' do before do - visit project_branches_path(project) + visit project_branches_path(project, search: 'binary-encoding') end scenario 'shows download artifacts button' do diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index ad4527a0b74..d1f5623554d 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -5,12 +5,6 @@ describe 'Branches' do let(:project) { create(:project, :public, :repository) } let(:repository) { project.repository } - def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").click - find(".dropdown-input-field").set(branch_name) - click_on("Create wildcard #{branch_name}") - end - context 'logged in as developer' do before do sign_in(user) @@ -18,12 +12,10 @@ describe 'Branches' do end describe 'Initial branches page' do - it 'shows all the branches' do + it 'shows all the branches sorted by last updated by default' do visit project_branches_path(project) - repository.branches_sorted_by(:name).first(20).each do |branch| - expect(page).to have_content("#{branch.name}") - end + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc)) end it 'sorts the branches by name' do @@ -32,22 +24,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Name" - sorted = repository.branches_sorted_by(:name).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) - end - - it 'sorts the branches by last updated' do - visit project_branches_path(project) - - click_button "Last updated" # Open sorting dropdown - click_link "Last updated" - - sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name)) end it 'sorts the branches by oldest updated' do @@ -56,10 +33,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Oldest updated" - sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc)) end it 'avoids a N+1 query in branches index' do @@ -99,28 +73,6 @@ describe 'Branches' do expect(find('.all-branches')).to have_selector('li', count: 0) end end - - describe 'Delete protected branch' do - before do - project.add_user(user, :master) - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - project.add_user(user, :developer) - end - - it 'does not allow devleoper to removes protected branch', js: true do - visit project_branches_path(project) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_css('.btn-remove.disabled') - end - end end context 'logged in as master' do @@ -136,37 +88,6 @@ describe 'Branches' do expect(page).to have_content("Protected branches can be managed in project settings") end end - - describe 'Delete protected branch' do - before do - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - end - - it 'removes branch after modal confirmation', js: true do - visit project_branches_path(project) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - page.find('[data-target="#modal-delete-branch"]').trigger(:click) - - expect(page).to have_css('.js-delete-branch[disabled]') - fill_in 'delete_branch_input', with: 'fix' - click_link 'Delete protected branch' - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('No branches to show') - end - end end context 'logged out' do @@ -180,4 +101,13 @@ describe 'Branches' do end end end + + def sorted_branches(repository, count:, sort_by:) + sorted_branches = + repository.branches_sorted_by(sort_by).first(count).map do |branch| + Regexp.escape(branch.name) + end + + Regexp.new(sorted_branches.join('.*')) + end end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index d2789d0aa52..1f9b52dd998 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' feature 'issuable templates', js: true do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } + let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' } before do project.team << [user, :master] @@ -28,14 +29,17 @@ feature 'issuable templates', js: true do longtemplate_content, message: 'added issue template', branch_name: 'master') - visit edit_project_issue_path project, issue - fill_in :'issue[title]', with: 'test issue title' + visit project_issue_path project, issue + page.within('.content .issuable-actions') do + click_on 'Edit' + end + fill_in :'issue-title', with: 'test issue title' end scenario 'user selects "bug" template' do select_template 'bug' wait_for_requests - assert_template + assert_template(page_part: issue_form_location) save_changes end @@ -43,30 +47,19 @@ feature 'issuable templates', js: true do select_template 'bug' wait_for_requests select_option 'No template' - assert_template('') + assert_template(expected_content: '', page_part: issue_form_location) save_changes('') end scenario 'user selects "bug" template, edits description and then selects "reset template"' do select_template 'bug' wait_for_requests - find_field('issue_description').send_keys(description_addition) - assert_template(template_content + description_addition) + find_field('issue-description').send_keys(description_addition) + assert_template(expected_content: template_content + description_addition, page_part: issue_form_location) select_option 'Reset template' - assert_template + assert_template(page_part: issue_form_location) save_changes end - - it 'updates height of markdown textarea' do - start_height = page.evaluate_script('$(".markdown-area").outerHeight()') - - select_template 'test' - wait_for_requests - - end_height = page.evaluate_script('$(".markdown-area").outerHeight()') - - expect(end_height).not_to eq(start_height) - end end context 'user creates an issue using templates, with a prior description' do @@ -81,15 +74,18 @@ feature 'issuable templates', js: true do template_content, message: 'added issue template', branch_name: 'master') - visit edit_project_issue_path project, issue - fill_in :'issue[title]', with: 'test issue title' - fill_in :'issue[description]', with: prior_description + visit project_issue_path project, issue + page.within('.content .issuable-actions') do + click_on 'Edit' + end + fill_in :'issue-title', with: 'test issue title' + fill_in :'issue-description', with: prior_description end scenario 'user selects "bug" template' do select_template 'bug' wait_for_requests - assert_template("#{template_content}") + assert_template(page_part: issue_form_location) save_changes end end @@ -154,8 +150,10 @@ feature 'issuable templates', js: true do end end - def assert_template(expected_content = template_content) - expect(find('textarea')['value']).to eq(expected_content) + def assert_template(expected_content: template_content, page_part: '#content-body') + page.within(page_part) do + expect(find('textarea')['value']).to eq(expected_content) + end end def save_changes(expected_content = template_content) diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 5d77cd1ccd5..06568817757 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -32,6 +32,32 @@ describe 'Edit Project Settings' do end end + describe 'Merge request settings section' do + it 'shows "Merge commit" strategy' do + visit edit_project_path(project) + + page.within '.merge-requests-feature' do + expect(page).to have_content 'Merge commit' + end + end + + it 'shows "Merge commit with semi-linear history " strategy' do + visit edit_project_path(project) + + page.within '.merge-requests-feature' do + expect(page).to have_content 'Merge commit with semi-linear history' + end + end + + it 'shows "Fast-forward merge" strategy' do + visit edit_project_path(project) + + page.within '.merge-requests-feature' do + expect(page).to have_content 'Fast-forward merge' + end + end + end + describe 'Rename repository section' do context 'with invalid characters' do it 'shows errors for invalid project path/name' do diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index 49ba2969ef0..470391dc66b 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -83,7 +83,7 @@ describe 'User views a wiki page' do it 'shows a file stored in a page' do file = Gollum::File.new(project.wiki) - allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master', true).and_return(file) + allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master').and_return(file) allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg') expect(page).to have_xpath('//img[@data-src="image.jpg"]') diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 3677bf38724..bf9885f73bd 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,93 +1,153 @@ require 'spec_helper' -feature 'Protected Branches', js: true do - let(:user) { create(:user, :admin) } +feature 'Protected Branches', :js do + let(:user) { create(:user) } + let(:admin) { create(:admin) } let(:project) { create(:project, :repository) } - before do - sign_in(user) - end + context 'logged in as developer' do + before do + project.add_developer(user) + sign_in(user) + end - def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").trigger('click') - find(".dropdown-input-field").set(branch_name) - click_on("Create wildcard #{branch_name}") - end + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end + + it 'does not allow developer to removes protected branch' do + visit project_branches_path(project) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) - describe "explicit protected branches" do - it "allows creating explicit protected branches" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + expect(page).to have_css('.btn-remove.disabled') + end + end + end - within(".protected-branches-list") { expect(page).to have_content('some-branch') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('some-branch') + context 'logged in as master' do + before do + project.add_master(user) + sign_in(user) end - it "displays the last commit on the matching branch if it exists" do - commit = create(:commit, project: project) - project.repository.add_branch(user, 'some-branch', commit.id) + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + it 'removes branch after modal confirmation' do + visit project_branches_path(project) - within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } - end + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) - it "displays an error message if the named branch does not exist" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + page.find('[data-target="#modal-delete-branch"]').trigger(:click) - within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + expect(page).to have_css('.js-delete-branch[disabled]') + fill_in 'delete_branch_input', with: 'fix' + click_link 'Delete protected branch' + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('No branches to show') + end end end - describe "wildcard protected branches" do - it "allows creating protected branches with a wildcard" do - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('*-stable') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('*-stable') + context 'logged in as admin' do + before do + sign_in(admin) end - it "displays the number of matching branches" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" + within(".protected-branches-list") { expect(page).to have_content('some-branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some-branch') + end - within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(admin, 'some-branch', commit.id) + + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } + end + + it "displays an error message if the named branch does not exist" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + end end - it "displays all the branches matching the wildcard" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') - project.repository.add_branch(user, 'development', 'master') + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" + it "displays the number of matching branches" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') - visit project_protected_branches_path(project) - click_on "2 matching branches" + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" - within(".protected-branches-list") do - expect(page).to have_content("production-stable") - expect(page).to have_content("staging-stable") - expect(page).not_to have_content("development") + within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + project.repository.add_branch(admin, 'development', 'master') + + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + visit project_protected_branches_path(project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end + + describe "access control" do + include_examples "protected branches > access control > CE" end end - describe "access control" do - include_examples "protected branches > access control > CE" + def set_protected_branch_name(branch_name) + find(".js-protected-branch-select").trigger('click') + find(".dropdown-input-field").set(branch_name) + click_on("Create wildcard #{branch_name}") end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index a7928857b7d..d70cf1527e7 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -181,21 +181,6 @@ describe "Internal Project Access" do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/issues/:id/edit" do - let(:issue) { create(:issue, project: project) } - subject { edit_project_issue_path(project, issue) } - - it { is_expected.to be_allowed_for(:admin) } - it { is_expected.to be_allowed_for(:owner).of(project) } - it { is_expected.to be_allowed_for(:master).of(project) } - it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } - it { is_expected.to be_denied_for(:guest).of(project) } - it { is_expected.to be_denied_for(:user) } - it { is_expected.to be_denied_for(:external) } - it { is_expected.to be_denied_for(:visitor) } - end - describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index a4396b20afd..ea130606545 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -181,21 +181,6 @@ describe "Private Project Access" do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/issues/:id/edit" do - let(:issue) { create(:issue, project: project) } - subject { edit_project_issue_path(project, issue) } - - it { is_expected.to be_allowed_for(:admin) } - it { is_expected.to be_allowed_for(:owner).of(project) } - it { is_expected.to be_allowed_for(:master).of(project) } - it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } - it { is_expected.to be_denied_for(:guest).of(project) } - it { is_expected.to be_denied_for(:user) } - it { is_expected.to be_denied_for(:external) } - it { is_expected.to be_denied_for(:visitor) } - end - describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index fccdeb0e5b7..d15f5af66c9 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -394,21 +394,6 @@ describe "Public Project Access" do it { is_expected.to be_allowed_for(:visitor) } end - describe "GET /:project_path/issues/:id/edit" do - let(:issue) { create(:issue, project: project) } - subject { edit_project_issue_path(project, issue) } - - it { is_expected.to be_allowed_for(:admin) } - it { is_expected.to be_allowed_for(:owner).of(project) } - it { is_expected.to be_allowed_for(:master).of(project) } - it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } - it { is_expected.to be_denied_for(:guest).of(project) } - it { is_expected.to be_denied_for(:user) } - it { is_expected.to be_denied_for(:external) } - it { is_expected.to be_denied_for(:visitor) } - end - describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index b6367b88e17..917fad74ef1 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -24,6 +24,24 @@ feature 'Signup' do end end + context "when sigining up with different cased emails" do + it "creates the user successfully" do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_email_confirmation', with: user.email.capitalize + fill_in 'new_user_password', with: user.password + click_button "Register" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + context "when not sending confirmation email" do before do stub_application_setting(send_user_confirmation_email: false) diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 1030f323a1f..30b4e56bc98 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -93,10 +93,12 @@ "merge_commit_message_with_description": { "type": "string" }, "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, - "remove_wip_path": { "type": "string" }, + "remove_wip_path": { "type": ["string", "null"] }, "commits_count": { "type": "integer" }, "remove_source_branch": { "type": ["boolean", "null"] }, - "merge_ongoing": { "type": "boolean" } + "merge_ongoing": { "type": "boolean" }, + "ff_only_enabled": { "type": ["boolean", false] }, + "should_be_rebased": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/registry/repositories.json b/spec/fixtures/api/schemas/registry/repositories.json new file mode 100644 index 00000000000..4978bd89cda --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repositories.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "repository.json" + } +} diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json new file mode 100644 index 00000000000..4175642eb00 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repository.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "required" : [ + "id", + "path", + "location", + "tags_path" + ], + "properties" : { + "id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "location": { + "type": "string" + }, + "tags_path": { + "type": "string" + }, + "destroy_path": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json new file mode 100644 index 00000000000..5bc307e0e64 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tag.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "required" : [ + "name", + "location" + ], + "properties" : { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "total_size": { + "type": "integer" + }, + "created_at": { + "type": "date" + }, + "destroy_path": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/registry/tags.json b/spec/fixtures/api/schemas/registry/tags.json new file mode 100644 index 00000000000..c72f957459a --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tags.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "tag.json" + } +} diff --git a/spec/fixtures/config/kubeconfig.yml b/spec/fixtures/config/kubeconfig.yml index c4e8e573c32..5152dae0104 100644 --- a/spec/fixtures/config/kubeconfig.yml +++ b/spec/fixtures/config/kubeconfig.yml @@ -4,7 +4,7 @@ clusters: - name: gitlab-deploy cluster: server: https://kube.domain.com - certificate-authority-data: "UEVN\n" + certificate-authority-data: "UEVN" contexts: - name: gitlab-deploy context: diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 36031ac1a28..76e5964ccf7 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -17,7 +17,7 @@ describe GroupsHelper do it 'gives default avatar_icon when no avatar is present' do group = create(:group) group.save! - expect(group_icon(group.path)).to match('group_avatar.png') + expect(group_icon(group.path)).to match_asset_path('group_avatar.png') end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 9aca3987657..baf927a9acc 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -54,7 +54,7 @@ describe PageLayoutHelper do describe 'page_image' do it 'defaults to the GitLab logo' do - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png' end %w(project user group).each do |type| @@ -70,13 +70,13 @@ describe PageLayoutHelper do object = double(avatar_url: nil) assign(type, object) - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png' end end context "with no assignments" do it 'falls back to the default' do - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png' end end end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 4bc2205e642..3fd16d76f51 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end + it 'merge_requests/merge_request_of_current_user.html.raw' do |example| + merge_request.update(author: admin) + + render_merge_request(example.description, merge_request) + end + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| create(:ci_build, :pending, pipeline: pipeline) diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js index 2d386fe1da5..2d386fe1da5 100644 --- a/spec/javascripts/notes/stores/helpers.js +++ b/spec/javascripts/helpers/vuex_action_helper.js diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/javascripts/locale/sprintf_spec.js new file mode 100644 index 00000000000..52e903b819f --- /dev/null +++ b/spec/javascripts/locale/sprintf_spec.js @@ -0,0 +1,74 @@ +import sprintf from '~/locale/sprintf'; + +describe('locale', () => { + describe('sprintf', () => { + it('does not modify string without parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('ignores extraneous parameters', () => { + const input = 'No parameters'; + + const output = sprintf(input, { ignore: 'this' }); + + expect(output).toBe(input); + }); + + it('ignores extraneous placeholders', () => { + const input = 'No %{parameters}'; + + const output = sprintf(input); + + expect(output).toBe(input); + }); + + it('replaces parameters', () => { + const input = '%{name} has %{count} parameters'; + const parameters = { + name: 'this', + count: 2, + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('this has 2 parameters'); + }); + + it('replaces multiple occurrences', () => { + const input = 'to %{verb} or not to %{verb}'; + const parameters = { + verb: 'be', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('to be or not to be'); + }); + + it('escapes parameters', () => { + const input = 'contains %{userContent}'; + const parameters = { + userContent: '<script>alert("malicious!")</script>', + }; + + const output = sprintf(input, parameters); + + expect(output).toBe('contains <script>alert("malicious!")</script>'); + }); + + it('does not escape parameters for escapeParameters = false', () => { + const input = 'contains %{safeContent}'; + const parameters = { + safeContent: '<strong>bold attempt</strong>', + }; + + const output = sprintf(input, parameters, false); + + expect(output).toBe('contains <strong>bold attempt</strong>'); + }); + }); +}); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 6ff42e2378d..3ab901da6b6 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper'; expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); }); }); + + describe('hideCloseButton', () => { + describe('merge request of another user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the dropdown close item and selects the next item', () => { + const closeItem = this.el.querySelector('li.close-item'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + const reportItem = this.el.querySelector('li.report-item'); + + expect(closeItem).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + expect(reportItem).toHaveClass('droplab-item-selected'); + expect(reportItem).not.toHaveClass('hidden'); + }); + }); + + describe('merge request of current_user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the close button', () => { + const closeButton = this.el.querySelector('.btn-close'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + + expect(closeButton).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + }); + }); + }); }); }).call(window); diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index 1c8b1b98242..3f659af5c3b 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -33,6 +33,30 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); }); + describe('handleSave', () => { + it('should request to save note when note is entered', () => { + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + spyOn(vm, 'resizeTextarea'); + spyOn(vm, 'stopPolling'); + + vm.handleSave(); + expect(vm.isSubmitting).toEqual(true); + expect(vm.note).toEqual(''); + expect(vm.saveNote).toHaveBeenCalled(); + expect(vm.stopPolling).toHaveBeenCalled(); + expect(vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + spyOn(vm, 'toggleIssueState'); + + vm.handleSave(); + + expect(vm.toggleIssueState).toHaveBeenCalled(); + }); + }); + describe('textarea', () => { it('should render textarea with placeholder', () => { expect( @@ -40,6 +64,22 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here...'); }); + it('should make textarea disabled while requesting', (done) => { + const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); + vm.note = 'hello world'; + spyOn(vm, 'stopPolling'); + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + + vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); + done(); + }); + }); + }); + it('should support quick actions', () => { expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 2b2219dcf0c..3d1ca870ca4 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,5 +1,5 @@ import * as actions from '~/notes/stores/actions'; -import testAction from './helpers'; +import testAction from '../../helpers/vuex_action_helper'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js new file mode 100644 index 00000000000..43e7d9e1224 --- /dev/null +++ b/spec/javascripts/registry/components/app_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import registry from '~/registry/components/app.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { reposServerResponse } from '../mock_data'; + +describe('Registry List', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(registry); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a list of repos', (done) => { + setTimeout(() => { + expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.container-image').length, + ).toEqual(reposServerResponse.length); + done(); + }); + }, 0); + }); + + describe('delete repository', () => { + it('should be possible to delete a repo', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined(); + done(); + }); + }, 0); + }); + }); + + describe('toggle repository', () => { + it('should open the container', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + }, 0); + }); + }); + }); + + describe('without data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render empty message', (done) => { + setTimeout(() => { + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toEqual('No container images stored for this project. Add one by following the instructions above.'); + done(); + }, 0); + }); + }); + + describe('while loading data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a loading spinner', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js new file mode 100644 index 00000000000..5891921318a --- /dev/null +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import collapsibleComponent from '~/registry/components/collapsible_container.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('collapsible registry container', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(collapsibleComponent); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggle', () => { + it('should be closed by default', () => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + }); + + it('should be open when user clicks on closed repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + + it('should be closed when the user clicks on an opened repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + done(); + }); + }); + }); + }); + + describe('delete repo', () => { + it('should be possible to delete a repo', () => { + expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js new file mode 100644 index 00000000000..6aa61afc445 --- /dev/null +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import tableRegistry from '~/registry/components/table_registry.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('table registry', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(tableRegistry); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a table with the registry list', () => { + expect( + vm.$el.querySelectorAll('table tbody tr').length, + ).toEqual(repoPropsData.list.length); + }); + + it('should render registry tag', () => { + const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' '); + expect(textRendered).toContain(repoPropsData.list[0].tag); + expect(textRendered).toContain(repoPropsData.list[0].shortRevision); + expect(textRendered).toContain(repoPropsData.list[0].layers); + expect(textRendered).toContain(repoPropsData.list[0].size); + }); + + it('should be possible to delete a registry', () => { + expect( + vm.$el.querySelector('.table tbody tr .js-delete-registry'), + ).toBeDefined(); + }); + + describe('pagination', () => { + it('should be possible to change the page', () => { + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js new file mode 100644 index 00000000000..3d989541881 --- /dev/null +++ b/spec/javascripts/registry/getters_spec.js @@ -0,0 +1,43 @@ +import * as getters from '~/registry/stores/getters'; + +describe('Getters Registry Store', () => { + let state; + + beforeEach(() => { + state = { + isLoading: false, + endpoint: '/root/empty-project/container_registry.json', + repos: [{ + canDelete: true, + destroyPath: 'bar', + id: '134', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab/foo', + tagsPath: 'foo', + }, { + canDelete: true, + destroyPath: 'bar', + id: '123', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab', + tagsPath: 'foo', + }], + }; + }); + + describe('isLoading', () => { + it('should return the isLoading property', () => { + expect(getters.isLoading(state)).toEqual(state.isLoading); + }); + }); + + describe('repos', () => { + it('should return the repos', () => { + expect(getters.repos(state)).toEqual(state.repos); + }); + }); +}); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js new file mode 100644 index 00000000000..18600d00bff --- /dev/null +++ b/spec/javascripts/registry/mock_data.js @@ -0,0 +1,122 @@ +export const defaultState = { + isLoading: false, + endpoint: '', + repos: [], +}; + +export const reposServerResponse = [ + { + destroy_path: 'path', + id: '123', + location: 'location', + path: 'foo', + tags_path: 'tags_path', + }, + { + destroy_path: 'path_', + id: '456', + location: 'location_', + path: 'bar', + tags_path: 'tags_path_', + }, +]; + +export const registryServerResponse = [ + { + name: 'centos7', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + destroy_path: 'path_', + }, + { + name: 'centos6', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + }]; + +export const parsedReposServerResponse = [ + { + canDelete: true, + destroyPath: reposServerResponse[0].destroy_path, + id: reposServerResponse[0].id, + isLoading: false, + list: [], + location: reposServerResponse[0].location, + name: reposServerResponse[0].path, + tagsPath: reposServerResponse[0].tags_path, + }, + { + canDelete: true, + destroyPath: reposServerResponse[1].destroy_path, + id: reposServerResponse[1].id, + isLoading: false, + list: [], + location: reposServerResponse[1].location, + name: reposServerResponse[1].path, + tagsPath: reposServerResponse[1].tags_path, + }, +]; + +export const parsedRegistryServerResponse = [ + { + tag: registryServerResponse[0].name, + revision: registryServerResponse[0].revision, + shortRevision: registryServerResponse[0].short_revision, + size: registryServerResponse[0].size, + layers: registryServerResponse[0].layers, + location: registryServerResponse[0].location, + createdAt: registryServerResponse[0].created_at, + destroyPath: registryServerResponse[0].destroy_path, + canDelete: true, + }, + { + tag: registryServerResponse[1].name, + revision: registryServerResponse[1].revision, + shortRevision: registryServerResponse[1].short_revision, + size: registryServerResponse[1].size, + layers: registryServerResponse[1].layers, + location: registryServerResponse[1].location, + createdAt: registryServerResponse[1].created_at, + destroyPath: registryServerResponse[1].destroy_path, + canDelete: false, + }, +]; + +export const repoPropsData = { + canDelete: true, + destroyPath: 'path', + id: '123', + isLoading: false, + list: [ + { + tag: 'centos6', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + shortRevision: 'b118ab5b0', + size: 19, + layers: 10, + location: 'location', + createdAt: 1505828744434, + destroyPath: 'path', + canDelete: true, + }, + ], + location: 'location', + name: 'foo', + tagsPath: 'path', + pagination: { + perPage: 5, + page: 1, + total: 13, + totalPages: 1, + nextPage: null, + previousPage: null, + }, +}; diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js new file mode 100644 index 00000000000..3c9da4f107b --- /dev/null +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import _ from 'underscore'; +import * as actions from '~/registry/stores/actions'; +import * as types from '~/registry/stores/mutation_types'; +import testAction from '../../helpers/vuex_action_helper'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, +} from '../mock_data'; + +Vue.use(VueResource); + +describe('Actions Registry Store', () => { + let interceptor; + let mockedState; + + beforeEach(() => { + mockedState = defaultState; + }); + + describe('server requests', () => { + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + describe('fetchRepos', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set receveived repos', (done) => { + testAction(actions.fetchRepos, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + { type: types.SET_REPOS_LIST, payload: reposServerResponse }, + ], done); + }); + }); + + describe('fetchList', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(registryServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set received list', (done) => { + mockedState.repos = parsedReposServerResponse; + + testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [ + { type: types.TOGGLE_REGISTRY_LIST_LOADING }, + { type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, + ], done); + }); + }); + }); + + describe('setMainEndpoint', () => { + it('should commit set main endpoint', (done) => { + testAction(actions.setMainEndpoint, 'endpoint', mockedState, [ + { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }, + ], done); + }); + }); + + describe('toggleLoading', () => { + it('should commit toggle main loading', (done) => { + testAction(actions.toggleLoading, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + ], done); + }); + }); +}); diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js new file mode 100644 index 00000000000..2e4c0659daa --- /dev/null +++ b/spec/javascripts/registry/stores/mutations_spec.js @@ -0,0 +1,81 @@ +import mutations from '~/registry/stores/mutations'; +import * as types from '~/registry/stores/mutation_types'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, + parsedRegistryServerResponse, +} from '../mock_data'; + +describe('Mutations Registry Store', () => { + let mockState; + beforeEach(() => { + mockState = defaultState; + }); + + describe('SET_MAIN_ENDPOINT', () => { + it('should set the main endpoint', () => { + const expectedState = Object.assign({}, mockState, { endpoint: 'foo' }); + mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo'); + expect(mockState).toEqual(expectedState); + }); + }); + + describe('SET_REPOS_LIST', () => { + it('should set a parsed repository list', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + expect(mockState.repos).toEqual(parsedReposServerResponse); + }); + }); + + describe('TOGGLE_MAIN_LOADING', () => { + it('should set a parsed repository list', () => { + mutations[types.TOGGLE_MAIN_LOADING](mockState); + expect(mockState.isLoading).toEqual(true); + }); + }); + + describe('SET_REGISTRY_LIST', () => { + it('should set a list of registries in a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse); + expect(mockState.repos[0].pagination).toEqual({ + perPage: 2, + page: 1, + total: 10, + totalPages: NaN, + nextPage: NaN, + previousPage: NaN, + }); + }); + }); + + describe('TOGGLE_REGISTRY_LIST_LOADING', () => { + it('should toggle isLoading property for a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]); + expect(mockState.repos[0].isLoading).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 47303d1e80f..d23b558f4ea 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -4,11 +4,15 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid const mr = { targetBranch: 'good-branch', targetBranchPath: '/good-branch', - closedBy: { - name: 'Fatih Acet', - username: 'fatihacet', + closedEvent: { + author: { + name: 'Fatih Acet', + username: 'fatihacet', + }, + updatedAt: 'closedEventUpdatedAt', + formattedUpdatedAt: '', }, - updatedAt: '2017-03-23T20:08:08.845Z', + updatedAt: 'mrUpdatedAt', closedAt: '1 day ago', }; @@ -18,7 +22,7 @@ const createComponent = () => { return new Component({ el: document.createElement('div'), propsData: { mr }, - }).$el; + }); }; describe('MRWidgetClosed', () => { @@ -38,14 +42,30 @@ describe('MRWidgetClosed', () => { }); describe('template', () => { - it('should have correct elements', () => { - const el = createComponent(); + let vm; + let el; + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should have correct elements', () => { expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name); + expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name); expect(el.textContent).toContain('The changes were not merged into'); expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); }); + + it('should use closedEvent updatedAt as tooltip title', () => { + expect( + el.querySelector('time').getAttribute('title'), + ).toBe('closedEventUpdatedAt'); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 3b7b7d93662..5d4c7ec09dc 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,20 +1,9 @@ import Vue from 'vue'; import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; +const ConflictsComponent = Vue.extend(conflictsComponent); const path = '/conflicts'; -const createComponent = () => { - const Component = Vue.extend(conflictsComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }, - }); -}; describe('MRWidgetConflicts', () => { describe('props', () => { @@ -27,44 +16,90 @@ describe('MRWidgetConflicts', () => { }); describe('template', () => { - it('should have correct elements', () => { - const el = createComponent().$el; - const resolveButton = el.querySelector('.js-resolve-conflicts-button'); - const mergeButton = el.querySelector('.mr-widget-body .btn'); - const mergeLocallyButton = el.querySelector('.js-merge-locally-button'); - - expect(el.textContent).toContain('There are merge conflicts'); - expect(el.textContent).not.toContain('ask someone with write access'); - expect(el.querySelector('.btn-success').disabled).toBeTruthy(); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); + describe('when allowed to merge', () => { + let vm; + + beforeEach(() => { + vm = mountComponent(ConflictsComponent, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); + }); + + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); + + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); + + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); }); describe('when user does not have permission to merge', () => { let vm; beforeEach(() => { - vm = createComponent(); - vm.mr.canMerge = false; + vm = mountComponent(ConflictsComponent, { + mr: { + canMerge: false, + }, + }); }); - it('should show proper message', (done) => { - Vue.nextTick(() => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - done(); - }); + afterEach(() => { + vm.$destroy(); + }); + + it('should show proper message', () => { + expect(vm.$el.textContent).toContain('ask someone with write access'); + }); + + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); }); + }); - it('should not have action buttons', (done) => { - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.btn').length).toBe(1); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null); - expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null); - done(); + describe('when fast-forward or semi-linear merge enabled', () => { + let vm; + + beforeEach(() => { + vm = mountComponent(ConflictsComponent, { + mr: { + shouldBeRebased: true, + }, }); }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); + }); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index afaa750199a..2714e8294fa 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -14,9 +14,12 @@ const createComponent = () => { canRevertInCurrentMR: true, canRemoveSourceBranch: true, sourceBranchRemoved: true, - mergedBy: {}, - mergedAt: '', - updatedAt: '', + mergedEvent: { + author: {}, + updatedAt: 'mergedUpdatedAt', + formattedUpdatedAt: '', + }, + updatedAt: 'mrUpdatedAt', targetBranch, }; @@ -170,5 +173,11 @@ describe('MRWidgetMerged', () => { done(); }); }); + + it('should use mergedEvent updatedAt as tooltip title', () => { + expect( + el.querySelector('time').getAttribute('title'), + ).toBe('mergedUpdatedAt'); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 03a52f1f91c..2422e844e97 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -182,36 +182,6 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); }); - - describe('Remove source branch checkbox', () => { - describe('when user can merge but cannot delete branch', () => { - it('isRemoveSourceBranchButtonDisabled should be true', () => { - expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); - }); - - it('should be disabled in the rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); - expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); - }); - }); - - describe('when user can merge and can delete branch', () => { - beforeEach(() => { - this.customVm = createComponent({ - mr: { canRemoveSourceBranch: true }, - }); - }); - - it('isRemoveSourceBranchButtonDisabled should be false', () => { - expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); - }); - - it('should be enabled in rendered output', () => { - const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); - expect(checkboxElement.getAttribute('disabled')).toBeNull(); - }); - }); - }); }); describe('methods', () => { @@ -467,4 +437,54 @@ describe('MRWidgetReadyToMerge', () => { }); }); }); + + describe('Remove source branch checkbox', () => { + describe('when user can merge but cannot delete branch', () => { + it('isRemoveSourceBranchButtonDisabled should be true', () => { + expect(vm.isRemoveSourceBranchButtonDisabled).toBe(true); + }); + + it('should be disabled in the rendered output', () => { + const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBe('disabled'); + }); + }); + + describe('when user can merge and can delete branch', () => { + beforeEach(() => { + this.customVm = createComponent({ + mr: { canRemoveSourceBranch: true }, + }); + }); + + it('isRemoveSourceBranchButtonDisabled should be false', () => { + expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false); + }); + + it('should be enabled in rendered output', () => { + const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input'); + expect(checkboxElement.getAttribute('disabled')).toBeNull(); + }); + }); + }); + + describe('Commit message area', () => { + it('when using merge commits, should show "Modify commit message" button', () => { + const customVm = createComponent({ + mr: { ffOnlyEnabled: false }, + }); + + expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull(); + expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + }); + + it('when fast-forward merge is enabled, only show fast-forward message', () => { + const customVm = createComponent({ + mr: { ffOnlyEnabled: true }, + }); + + expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined(); + expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + }); + }); }); diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index e6645985ba4..33540eab5d6 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -195,6 +195,32 @@ describe Gitlab::Ci::Ansi2html do end end + context "with section markers" do + let(:section_name) { 'test_section' } + let(:section_start_time) { Time.new(2017, 9, 20).utc } + let(:section_duration) { 3.seconds } + let(:section_end_time) { section_start_time + section_duration } + let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"} + let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"} + let(:section_start_html) do + '<div class="hidden" data-action="start"'\ + " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\ + "#{section_start[0...-5]}</div>" + end + let(:section_end_html) do + '<div class="hidden" data-action="end"'\ + " data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\ + "#{section_end[0...-5]}</div>" + end + + it "prints light red" do + text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" + html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + + expect(convert_html(text)).to eq(html) + end + end + describe "truncates" do let(:text) { "Hello World" } let(:stream) { StringIO.new(text) } diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 9e528392756..ef7d766a13d 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -254,6 +254,46 @@ describe Gitlab::ClosingIssueExtractor do expect(subject.closed_by_message(message)).to eq([issue]) end + it do + message = "Implement: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implements: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implemented: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "Implementing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implement: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implements: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implemented: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + + it do + message = "implementing: #{reference}" + expect(subject.closed_by_message(message)).to eq([issue]) + end + context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 3494f0cc98d..ee657101f4c 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -341,8 +341,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when diff is quite large will collapse by default' do - let(:iterator) { [{ diff: 'a' * (Gitlab::Git::Diff.collapse_limit + 1) }] } - let(:max_files) { 100 } + let(:iterator) { [{ diff: 'a' * 20480 }] } context 'when no collapse is set' do let(:expanded) { true } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index d39b33a0c05..4a7b06003fc 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -31,36 +31,6 @@ EOT [".gitmodules"]).patches.first end - describe 'size limit feature toggles' do - context 'when the feature gitlab_git_diff_size_limit_increase is enabled' do - before do - stub_feature_flags(gitlab_git_diff_size_limit_increase: true) - end - - it 'returns 200 KB for size_limit' do - expect(described_class.size_limit).to eq(200.kilobytes) - end - - it 'returns 100 KB for collapse_limit' do - expect(described_class.collapse_limit).to eq(100.kilobytes) - end - end - - context 'when the feature gitlab_git_diff_size_limit_increase is disabled' do - before do - stub_feature_flags(gitlab_git_diff_size_limit_increase: false) - end - - it 'returns 100 KB for size_limit' do - expect(described_class.size_limit).to eq(100.kilobytes) - end - - it 'returns 10 KB for collapse_limit' do - expect(described_class.collapse_limit).to eq(10.kilobytes) - end - end - end - describe '.new' do context 'using a Hash' do context 'with a small diff' do @@ -77,7 +47,7 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - diff = described_class.new(diff: 'a' * (described_class.size_limit + 1)) + diff = described_class.new(diff: 'a' * 204800) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -115,8 +85,8 @@ EOT # The patch total size is 200, with lines between 21 and 54. # This is a quick-and-dirty way to test this. Ideally, a new patch is # added to the test repo with a size that falls between the real limits. - allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(150) - allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(100) + stub_const("#{described_class}::SIZE_LIMIT", 150) + stub_const("#{described_class}::COLLAPSE_LIMIT", 100) end it 'prunes the diff as a large diff instead of as a collapsed diff' do @@ -356,7 +326,7 @@ EOT describe '#collapsed?' do it 'returns true for a diff that is quite large' do - diff = described_class.new({ diff: 'a' * (described_class.collapse_limit + 1) }, expanded: false) + diff = described_class.new({ diff: 'a' * 20480 }, expanded: false) expect(diff).to be_collapsed end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a0482e30a33..5f12125beb2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#rm_branch' do + shared_examples "user deleting a branch" do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:user) { create(:user) } + let(:branch_name) { "to-be-deleted-soon" } + + before do + project.team << [user, :developer] + repository.create_branch(branch_name) + end + + it "removes the branch from the repo" do + repository.rm_branch(branch_name, user: user) + + expect(repository.rugged.branches[branch_name]).to be_nil + end + end + + context "when Gitaly user_delete_branch is enabled" do + it_behaves_like "user deleting a branch" + end + + context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + end + end + + describe '#write_ref' do + context 'validations' do + using RSpec::Parameterized::TableSyntax + + where(:ref_path, :ref) do + 'foo bar' | '123' + 'foobar' | "12\x003" + end + + with_them do + it 'raises ArgumentError' do + expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError) + end + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb index ab64b041187..31d5f59a562 100644 --- a/spec/lib/gitlab/git/user_spec.rb +++ b/spec/lib/gitlab/git/user_spec.rb @@ -8,6 +8,20 @@ describe Gitlab::Git::User do subject { described_class.new(username, name, email, gl_id) } + describe '.from_gitaly' do + let(:gitaly_user) { Gitaly::User.new(name: name, email: email, gl_id: gl_id) } + subject { described_class.from_gitaly(gitaly_user) } + + it { expect(subject).to eq(described_class.new('', name, email, gl_id)) } + end + + describe '.from_gitlab' do + let(:user) { build(:user) } + subject { described_class.from_gitlab(user) } + + it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) } + end + describe '#==' do def eq_other(username, name, email, gl_id) eq(described_class.new(username, name, email, gl_id)) diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 1ef3e2e3a5d..b2275119a04 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -53,7 +53,7 @@ describe Gitlab::GitalyClient::CommitService do end it 'encodes paths correctly' do - expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt']) }.not_to raise_error + expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt', nil]) }.not_to raise_error end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 769b14687ac..7bd6a7fa842 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do let(:project) { create(:project) } let(:repository) { project.repository.raw } let(:client) { described_class.new(repository) } + let(:user) { create(:user) } + let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } describe '#user_create_branch' do - let(:user) { create(:user) } - let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } let(:branch_name) { 'new' } let(:start_point) { 'master' } let(:request) do @@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do end end end + + describe '#user_delete_branch' do + let(:branch_name) { 'my-branch' } + let(:request) do + Gitaly::UserDeleteBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + user: gitaly_user + ) + end + let(:response) { Gitaly::UserDeleteBranchResponse.new } + + subject { client.user_delete_branch(branch_name, user) } + + it 'sends a user_delete_branch message' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + subject + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::HooksService::PreReceiveError, "something failed") + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 9a84d6e6a67..a1f4e65b8d4 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -38,6 +38,20 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end + describe 'encode' do + [ + [nil, ""], + ["", ""], + [" ", " "], + %w(a1 a1), + ["编码", "\xE7\xBC\x96\xE7\xA0\x81".b] + ].each do |input, result| + it "encodes #{input.inspect} to #{result.inspect}" do + expect(described_class.encode(input)).to eq result + end + end + end + describe 'allow_n_plus_1_calls' do context 'when RequestStore is enabled', :request_store do it 'returns the result of the allow_n_plus_1_calls block' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 899d17d97c2..7268226112c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -412,6 +412,8 @@ Project: - last_repository_updated_at - ci_config_path - delete_error +- merge_requests_ff_only_enabled +- merge_requests_rebase_enabled Author: - name ProjectFeature: diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 8aaf320cbf5..db26e16e3b2 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::OAuth::User do let(:oauth_user) { described_class.new(auth_hash) } let(:gl_user) { oauth_user.gl_user } let(:uid) { 'my-uid' } + let(:dn) { 'uid=user1,ou=People,dc=example' } let(:provider) { 'my-provider' } let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } let(:info_hash) do @@ -197,7 +198,7 @@ describe Gitlab::OAuth::User do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(ldap_user).to receive(:dn) { dn } end context "and no account for the LDAP user" do @@ -213,7 +214,7 @@ describe Gitlab::OAuth::User do identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -221,7 +222,7 @@ describe Gitlab::OAuth::User do end context "and LDAP user has an account already" do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) @@ -234,7 +235,7 @@ describe Gitlab::OAuth::User do identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } expect(identities_as_hash).to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -252,7 +253,7 @@ describe Gitlab::OAuth::User do expect(identities_as_hash) .to match_array( [ - { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'ldapmain', extern_uid: dn }, { provider: 'twitter', extern_uid: uid } ] ) @@ -310,8 +311,8 @@ describe Gitlab::OAuth::User do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(oauth_user).to receive(:ldap_person).and_return(ldap_user) + allow(ldap_user).to receive(:dn) { dn } + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context "and no account for the LDAP user" do @@ -341,7 +342,7 @@ describe Gitlab::OAuth::User do end context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } context 'dont block on create (LDAP)' do before do diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 2f989397f7e..1f1c48ee9b5 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -84,9 +84,9 @@ describe Gitlab::PathRegex do let(:top_level_words) do words = routes_not_starting_in_wildcard.map do |route| route.split('/')[1] - end.compact.uniq + end.compact - words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s) + (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq end let(:ee_top_level_words) do @@ -95,10 +95,11 @@ describe Gitlab::PathRegex do let(:files_in_public) do git = Gitlab.config.git.bin_path - `cd #{Rails.root} && #{git} ls-files public` + tracked = `cd #{Rails.root} && #{git} ls-files public` .split("\n") .map { |entry| entry.gsub('public/', '') } .uniq + tracked + %w(assets uploads) end # All routes that start with a namespaced path, that have 1 or more diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 4567f220c11..b145ca36f26 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -14,7 +14,7 @@ describe 'Gitlab::Popen' do end it { expect(@status).to be_zero } - it { expect(@output).to include('cache') } + it { expect(@output).to include('tests') } end context 'non-zero status' do diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index 19710029224..59923bfb14d 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -1,9 +1,12 @@ require 'spec_helper' describe Gitlab::Saml::User do + include LdapHelpers + let(:saml_user) { described_class.new(auth_hash) } let(:gl_user) { saml_user.gl_user } let(:uid) { 'my-uid' } + let(:dn) { 'uid=user1,ou=People,dc=example' } let(:provider) { 'saml' } let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new({ 'groups' => %w(Developers Freelancers Designers) }) }) } let(:info_hash) do @@ -163,13 +166,17 @@ describe Gitlab::Saml::User do end context 'and a corresponding LDAP person' do + let(:adapter) { ldap_adapter('ldapmain') } + before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) + allow(ldap_user).to receive(:dn) { dn } + allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter) + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user) end context 'and no account for the LDAP user' do @@ -181,20 +188,86 @@ describe Gitlab::Saml::User do expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, { provider: 'saml', extern_uid: uid }]) end end context 'and LDAP user has an account already' do + let(:auth_hash_base_attributes) do + { + uid: uid, + provider: provider, + info: info_hash, + extra: { + raw_info: OneLogin::RubySaml::Attributes.new( + { 'groups' => %w(Developers Freelancers Designers) } + ) + } + } + end + let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) } + let(:uid_types) { %w(uid dn email) } + before do create(:omniauth_user, email: 'john@mail.com', - extern_uid: 'uid=user1,ou=People,dc=example', + extern_uid: dn, provider: 'ldapmain', username: 'john') end + shared_examples 'find LDAP person' do |uid_type, uid| + let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes.merge(uid: extern_uid)) } + + before do + nil_types = uid_types - [uid_type] + + nil_types.each do |type| + allow(Gitlab::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil) + end + + allow(Gitlab::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user) + end + + it 'adds the omniauth identity to the LDAP account' do + identities = [ + { provider: 'ldapmain', extern_uid: dn }, + { provider: 'saml', extern_uid: extern_uid } + ] + + identities_as_hash = gl_user.identities.map do |id| + { provider: id.provider, extern_uid: id.extern_uid } + end + + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@mail.com' + expect(gl_user.identities.length).to be 2 + expect(identities_as_hash).to match_array(identities) + end + end + + context 'when uid is an uid' do + it_behaves_like 'find LDAP person', 'uid' do + let(:extern_uid) { uid } + end + end + + context 'when uid is a dn' do + it_behaves_like 'find LDAP person', 'dn' do + let(:extern_uid) { dn } + end + end + + context 'when uid is an email' do + it_behaves_like 'find LDAP person', 'email' do + let(:extern_uid) { 'john@mail.com' } + end + end + it 'adds the omniauth identity to the LDAP account' do saml_user.save @@ -203,7 +276,7 @@ describe Gitlab::Saml::User do expect(gl_user.email).to eql 'john@mail.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, { provider: 'saml', extern_uid: uid }]) end @@ -219,17 +292,21 @@ describe Gitlab::Saml::User do context 'user has SAML user, and wants to add their LDAP identity' do it 'adds the LDAP identity to the existing SAML user' do - create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') - local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash) + create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john') + + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user) + + local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash) local_saml_user = described_class.new(local_hash) + local_saml_user.save local_gl_user = local_saml_user.gl_user expect(local_gl_user).to be_valid expect(local_gl_user.identities.length).to be 2 identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, - { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }]) + expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn }, + { provider: 'saml', extern_uid: dn }]) end end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index baf8f6644bf..8026fba9f0a 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end + + it 'uses UNION ALL when removing duplicates is disabled' do + union = described_class + .new([relation_1, relation_2], remove_duplicates: false) + + expect(union.to_sql).to include('UNION ALL') + end end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 59c28431e1e..fc8991fd31f 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -39,7 +39,8 @@ describe Gitlab::UrlSanitizer do false | nil false | '' false | '123://invalid:url' - true | 'valid@project:url.git' + false | 'valid@project:url.git' + false | 'valid:pass@project:url.git' true | 'ssh://example.com' true | 'ssh://:@example.com' true | 'ssh://foo@example.com' @@ -81,24 +82,6 @@ describe Gitlab::UrlSanitizer do describe '#credentials' do context 'credentials in hash' do - where(:input, :output) do - { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' } - { user: 'foo', password: '' } | { user: 'foo', password: nil } - { user: 'foo', password: nil } | { user: 'foo', password: nil } - { user: '', password: 'bar' } | { user: nil, password: 'bar' } - { user: '', password: '' } | { user: nil, password: nil } - { user: '', password: nil } | { user: nil, password: nil } - { user: nil, password: 'bar' } | { user: nil, password: 'bar' } - { user: nil, password: '' } | { user: nil, password: nil } - { user: nil, password: nil } | { user: nil, password: nil } - end - - with_them do - subject { described_class.new('user@example.com:path.git', credentials: input).credentials } - - it { is_expected.to eq(output) } - end - it 'overrides URL-provided credentials' do sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' }) @@ -116,10 +99,6 @@ describe Gitlab::UrlSanitizer do 'http://@example.com' | { user: nil, password: nil } 'http://example.com' | { user: nil, password: nil } - # Credentials from SCP-style URLs are not supported at present - 'foo@example.com:path' | { user: nil, password: nil } - 'foo:bar@example.com:path' | { user: nil, password: nil } - # Other invalid URLs nil | { user: nil, password: nil } '' | { user: nil, password: nil } diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 72496e9a212..4dffe2bd82f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -13,13 +13,51 @@ describe Gitlab::Workhorse do end describe ".send_git_archive" do + let(:ref) { 'master' } + let(:format) { 'zip' } + let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path } + let(:base_params) { repository.archive_metadata(ref, storage_path, format) } + let(:gitaly_params) do + base_params.merge( + 'GitalyServer' => { + 'address' => Gitlab::GitalyClient.address(project.repository_storage), + 'token' => Gitlab::GitalyClient.token(project.repository_storage) + }, + 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys + ) + end + + subject do + described_class.send_git_archive(repository, ref: ref, format: format) + end + + context 'when Gitaly workhorse_archive feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to include(gitaly_params) + end + end + + context 'when Gitaly workhorse_archive feature is disabled', skip_gitaly_mock: true do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to eq(base_params) + end + end + context "when the repository doesn't have an archive file path" do before do allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) end it "raises an error" do - expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) + expect { subject }.to raise_error(RuntimeError) end end end diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb index 7125bfcab59..a0fb86345f3 100644 --- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -16,7 +16,12 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do end it 'only whitelists safe files' do - expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts') + expect(described_class::WHITELIST).to contain_exactly( + 'authorized_keys', + 'authorized_keys2', + 'authorized_keys.lock', + 'known_hosts' + ) end describe '#skip?' do diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index fadc8bfeb61..4a4d079b721 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -138,6 +138,14 @@ describe GpgKey do expect(gpg_key.verified?).to be_truthy expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.com')).to be_truthy end + + it 'returns true if one of the email addresses in the key belongs to the user and case-insensitively matches the provided email' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_truthy + expect(gpg_key.verified_and_belongs_to_email?('Bette.Cartwright@example.com')).to be_truthy + end end describe '#revoke' do diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 8eabc4ca72f..81c2057e175 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -155,5 +155,15 @@ describe Key, :mailer do it 'strips white spaces' do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end + + it 'invalidates the public_key attribute' do + key = build(:key) + + original = key.public_key + key.key = valid_key + + expect(original.key_text).not_to be_nil + expect(key.public_key.key_text).to eq(valid_key) + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d80d5657c42..188a0a98ec3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -791,6 +791,49 @@ describe MergeRequest do end end + describe '#has_ci?' do + let(:merge_request) { build_stubbed(:merge_request) } + + context 'has ci' do + it 'returns true if MR has head_pipeline_id and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { double } + allow(merge_request).to receive(:has_no_commits?) { false } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has any pipeline and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [double] } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has CI service and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [] } + + expect(merge_request.has_ci?).to be(true) + end + end + + context 'has no ci' do + it 'returns false if MR has no CI service nor pipeline, and no commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:all_pipelines) { [] } + allow(merge_request).to receive(:has_no_commits?) { true } + + expect(merge_request.has_ci?).to be(false) + end + end + end + describe '#all_pipelines' do shared_examples 'returning pipelines with proper ordering' do let!(:all_pipelines) do diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 537cdadd528..2298dcab55f 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -208,7 +208,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do config.dig('users', 0, 'user')['token'] = 'token' config.dig('contexts', 0, 'context')['namespace'] = namespace config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = - Base64.encode64('CA PEM DATA') + Base64.strict_encode64('CA PEM DATA') YAML.dump(config) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 868a843ab0a..176bb568cbe 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -408,6 +408,18 @@ describe Project do end end + describe '#merge_method' do + it 'returns "ff" merge_method when ff is enabled' do + project = build(:project, merge_requests_ff_only_enabled: true) + expect(project.merge_method).to be :ff + end + + it 'returns "merge" merge_method when ff is disabled' do + project = build(:project, merge_requests_ff_only_enabled: false) + expect(project.merge_method).to be :merge + end + end + describe '#repository_storage_path' do let(:project) { create(:project, repository_storage: 'custom') } @@ -2793,6 +2805,17 @@ describe Project do end end + describe '#check_repository_path_availability' do + let(:project) { build(:project) } + + it 'skips gitlab-shell exists?' do + project.skip_disk_validation = true + + expect(project.gitlab_shell).not_to receive(:exists?) + expect(project.check_repository_path_availability).to be_truthy + end + end + describe '#latest_successful_pipeline_for_default_branch' do let(:project) { build(:project) } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 953df7746eb..78fb2df884a 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -6,13 +6,10 @@ describe ProjectWiki do let(:user) { project.owner } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_wiki) { described_class.new(project, user) } + let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') } subject { project_wiki } - before do - project_wiki.wiki - end - describe "#path_with_namespace" do it "returns the project path with namespace with the .wiki extension" do expect(subject.path_with_namespace).to eq(project.full_path + '.wiki') @@ -61,8 +58,8 @@ describe ProjectWiki do end describe "#wiki" do - it "contains a Gollum::Wiki instance" do - expect(subject.wiki).to be_a Gollum::Wiki + it "contains a Gitlab::Git::Wiki instance" do + expect(subject.wiki).to be_a Gitlab::Git::Wiki end it "creates a new wiki repo if one does not yet exist" do @@ -70,20 +67,18 @@ describe ProjectWiki do end it "raises CouldNotCreateWikiError if it can't create the wiki repository" do - allow(project_wiki).to receive(:init_repo).and_return(false) - expect { project_wiki.send(:create_repo!) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError) + # Create a fresh project which will not have a wiki + project_wiki = described_class.new(create(:project), user) + gitlab_shell = double(:gitlab_shell) + allow(gitlab_shell).to receive(:add_repository) + allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell) + + expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError) end end describe "#empty?" do context "when the wiki repository is empty" do - before do - allow_any_instance_of(Gitlab::Shell).to receive(:add_repository) do - create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git") - end - allow(project).to receive(:full_path).and_return("non-existant") - end - describe '#empty?' do subject { super().empty? } it { is_expected.to be_truthy } @@ -154,13 +149,13 @@ describe ProjectWiki do before do file = Gollum::File.new(subject.wiki) allow_any_instance_of(Gollum::Wiki) - .to receive(:file).with('image.jpg', 'master', true) + .to receive(:file).with('image.jpg', 'master') .and_return(file) allow_any_instance_of(Gollum::File) .to receive(:mime_type) .and_return('image/jpeg') allow_any_instance_of(Gollum::Wiki) - .to receive(:file).with('non-existant', 'master', true) + .to receive(:file).with('non-existant', 'master') .and_return(nil) end @@ -178,9 +173,9 @@ describe ProjectWiki do expect(subject.find_file('non-existant')).to eq(nil) end - it 'returns a Gollum::File instance' do + it 'returns a Gitlab::Git::WikiFile instance' do file = subject.find_file('image.jpg') - expect(file).to be_a Gollum::File + expect(file).to be_a Gitlab::Git::WikiFile end end @@ -222,9 +217,9 @@ describe ProjectWiki do describe "#update_page" do before do create_page("update-page", "some content") - @gollum_page = subject.wiki.paged("update-page") + @gitlab_git_wiki_page = subject.wiki.page(title: "update-page") subject.update_page( - @gollum_page, + @gitlab_git_wiki_page, content: "some other content", format: :markdown, message: "updated page" @@ -246,7 +241,7 @@ describe ProjectWiki do it 'updates project activity' do subject.update_page( - @gollum_page, + @gitlab_git_wiki_page, content: 'Yet more content', format: :markdown, message: 'Updated page again' @@ -262,7 +257,7 @@ describe ProjectWiki do describe "#delete_page" do before do create_page("index", "some content") - @page = subject.wiki.paged("index") + @page = subject.wiki.page(title: "index") end it "deletes the page" do @@ -282,27 +277,28 @@ describe ProjectWiki do describe '#create_repo!' do it 'creates a repository' do - expect(subject).to receive(:init_repo) - .with(subject.full_path) - .and_return(true) - + expect(raw_repository.exists?).to eq(false) expect(subject.repository).to receive(:after_create) - expect(subject.create_repo!).to be_an_instance_of(Gollum::Wiki) + subject.send(:create_repo!, raw_repository) + + expect(raw_repository.exists?).to eq(true) end end describe '#ensure_repository' do it 'creates the repository if it not exist' do - allow(subject).to receive(:repository_exists?).and_return(false) - - expect(subject).to receive(:create_repo!) + expect(raw_repository.exists?).to eq(false) + expect(subject).to receive(:create_repo!).and_call_original subject.ensure_repository + + expect(raw_repository.exists?).to eq(true) end it 'does not create the repository if it exists' do - allow(subject).to receive(:repository_exists?).and_return(true) + subject.wiki + expect(raw_repository.exists?).to eq(true) expect(subject).not_to receive(:create_repo!) @@ -329,7 +325,7 @@ describe ProjectWiki do end def commit_details - { name: user.name, email: user.email, message: "test commit" } + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") end def create_page(name, content) @@ -337,6 +333,6 @@ describe ProjectWiki do end def destroy_page(page) - subject.wiki.delete_page(page, commit_details) + subject.delete_page(page, commit_details) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 157a10edd73..7156c1b7aa8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -636,18 +636,18 @@ describe Repository do describe '#fetch_ref' do describe 'when storage is broken', broken_storage: true do it 'should raise a storage error' do - path = broken_repository.path_to_repo - - expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } + expect_to_raise_storage_error do + broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2') + end end end end describe '#create_ref' do - it 'redirects the call to fetch_ref' do + it 'redirects the call to write_ref' do ref, ref_path = '1', '2' - expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path) + expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref) repository.create_ref(ref, ref_path) end @@ -901,47 +901,6 @@ describe Repository do end end - describe '#rm_branch' do - let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature - let(:blank_sha) { '0000000000000000000000000000000000000000' } - - context 'when pre hooks were successful' do - it 'runs without errors' do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - end - - it 'deletes the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - - expect(repository.find_branch('feature')).to be_nil - end - end - - context 'when pre hooks failed' do - it 'gets an error' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - end - - it 'does not delete the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - expect(repository.find_branch('feature')).not_to be_nil - end - end - end - describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev @@ -1345,6 +1304,34 @@ describe Repository do end end + describe '#ff_merge' do + before do + repository.add_branch(user, 'ff-target', 'feature~5') + end + + it 'merges the code and return the commit id' do + merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project) + merge_commit_id = repository.ff_merge(user, + merge_request.diff_head_sha, + merge_request.target_branch, + merge_request: merge_request) + merge_commit = repository.commit(merge_commit_id) + + expect(merge_commit).to be_present + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + + it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do + merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project) + merge_commit_id = repository.ff_merge(user, + merge_request.diff_head_sha, + merge_request.target_branch, + merge_request: merge_request) + + expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) + end + end + describe '#revert' do let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } @@ -1716,13 +1703,75 @@ describe Repository do end describe '#rm_branch' do - let(:user) { create(:user) } + shared_examples "user deleting a branch" do + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) - it 'removes a branch' do - expect(repository).to receive(:before_remove_branch) - expect(repository).to receive(:after_remove_branch) + repository.rm_branch(user, 'feature') + end + end + + context 'with gitaly enabled' do + it_behaves_like "user deleting a branch" - repository.rm_branch(user, 'feature') + context 'when pre hooks failed' do + before do + allow_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'gets an error and does not delete the branch' do + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + + expect(repository.find_branch('feature')).not_to be_nil + end + end + end + + context 'with gitaly disabled', skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + + context 'when pre hooks were successful' do + it 'runs without errors' do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + end + + it 'deletes the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + + expect(repository.find_branch('feature')).to be_nil + end + end + + context 'when pre hooks failed' do + it 'gets an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'does not delete the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + expect(repository.find_branch('feature')).not_to be_nil + end + end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 9ef8d117123..1f14d06997e 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -80,7 +80,7 @@ describe WikiPage do context "when initialized with an existing gollum page" do before do create_page("test page", "test content") - @page = wiki.wiki.paged("test page") + @page = wiki.wiki.page(title: "test page") @wiki_page = described_class.new(wiki, @page, true) end @@ -105,7 +105,7 @@ describe WikiPage do end it "sets the version attribute" do - expect(@wiki_page.version).to be_a Gollum::Git::Commit + expect(@wiki_page.version).to be_a Gitlab::Git::WikiPageVersion end end end @@ -321,14 +321,14 @@ describe WikiPage do end it 'returns true when requesting an old version' do - old_version = @page.versions.last.to_s + old_version = @page.versions.last.id old_page = wiki.find_page('Update', old_version) expect(old_page.historical?).to eq true end it 'returns false when requesting latest version' do - latest_version = @page.versions.first.to_s + latest_version = @page.versions.first.id latest_page = wiki.find_page('Update', latest_version) expect(latest_page.historical?).to eq false @@ -393,7 +393,7 @@ describe WikiPage do end def commit_details - { name: user.name, email: user.email, message: "test commit" } + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") end def create_page(name, content) @@ -401,8 +401,8 @@ describe WikiPage do end def destroy_page(title) - page = wiki.wiki.paged(title) - wiki.wiki.delete_page(page, commit_details) + page = wiki.wiki.page(title: title) + wiki.delete_page(page, commit_details) end def get_slugs(page_or_dir) diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 2187be0190d..5e114434a67 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -300,6 +300,10 @@ describe MergeRequestPresenter do described_class.new(resource, current_user: user).remove_wip_path end + before do + allow(resource).to receive(:work_in_progress?).and_return(true) + end + context 'when merge request enabled and has permission' do it 'has remove_wip_path' do allow(project).to receive(:merge_requests_enabled?) { true } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 12720355a6d..5068df5b43a 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -360,6 +360,8 @@ describe API::Runner do 'policy' => 'pull-push' }] end + let(:expected_features) { { 'trace_sections' => true } } + it 'picks a job' do request_job info: { platform: :darwin } @@ -379,6 +381,7 @@ describe API::Runner do expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) expect(json_response['variables']).to include(*expected_variables) + expect(json_response['features']).to eq(expected_features) end context 'when job is made for tag' do diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 01e2cfed6f8..9673b11c2a2 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -38,7 +38,7 @@ describe BuildSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") end end end diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb new file mode 100644 index 00000000000..c589cd18f77 --- /dev/null +++ b/spec/serializers/container_repository_entity_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe ContainerRepositoryEntity do + let(:entity) do + described_class.new(repository, request: request) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:repository) { create(:container_repository, project: project) } + + let(:request) { double('request') } + + subject { entity.as_json } + + before do + stub_container_registry_config(enabled: true) + allow(request).to receive(:project).and_return(project) + allow(request).to receive(:current_user).and_return(user) + end + + it 'exposes required informations' do + expect(subject).to include(:id, :path, :location, :tags_path) + end + + context 'when user can manage repositories' do + before do + project.add_developer(user) + end + + it 'exposes destroy_path' do + expect(subject).to include(:destroy_path) + end + end + + context 'when user cannot manage repositories' do + it 'does not expose destroy_path' do + expect(subject).not_to include(:destroy_path) + end + end +end diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb new file mode 100644 index 00000000000..6dcc5204516 --- /dev/null +++ b/spec/serializers/container_tag_entity_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe ContainerTagEntity do + let(:entity) do + described_class.new(tag, request: request) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:repository) { create(:container_repository, name: 'image', project: project) } + + let(:request) { double('request') } + let(:tag) { repository.tag('test') } + + subject { entity.as_json } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[test]) + allow(request).to receive(:project).and_return(project) + allow(request).to receive(:current_user).and_return(user) + end + + it 'exposes required informations' do + expect(subject).to include(:name, :location, :revision, :total_size, :created_at) + end + + context 'when user can manage repositories' do + before do + project.add_developer(user) + end + + it 'exposes destroy_path' do + expect(subject).to include(:destroy_path) + end + end + + context 'when user cannot manage repositories' do + it 'does not expose destroy_path' do + expect(subject).not_to include(:destroy_path) + end + end +end diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index a2fd5b7daae..4aeb593da44 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -11,16 +11,6 @@ describe MergeRequestEntity do described_class.new(resource, request: request).as_json end - it 'includes author' do - req = double('request') - - author_payload = UserEntity - .represent(resource.author, request: req) - .as_json - - expect(subject[:author]).to eq(author_payload) - end - it 'includes pipeline' do req = double('request', current_user: user) pipeline = build_stubbed(:ci_pipeline) @@ -47,7 +37,8 @@ describe MergeRequestEntity do :cancel_merge_when_pipeline_succeeds_path, :create_issue_to_resolve_discussions_path, :source_branch_path, :target_branch_commits_path, - :target_branch_tree_path, :commits_count, :merge_ongoing) + :target_branch_tree_path, :commits_count, :merge_ongoing, + :ff_only_enabled) end it 'has email_patches_path' do diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 3baf9b1edab..8fc1ceedc34 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -168,7 +168,7 @@ describe PipelineSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to eq("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") end end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 3964b998084..16431ed4188 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,12 +18,12 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path - expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico') + expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico') end it 'contains a dev namespaced favicon if dev env' do allow(Rails.env).to receive(:development?) { true } - expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico') + expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico') end end end diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb new file mode 100644 index 00000000000..aaabf3ed2b0 --- /dev/null +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe MergeRequests::FfMergeService do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:merge_request) do + create(:merge_request, + source_branch: 'flatten-dir', + target_branch: 'improve/awesome', + assignee: user2) + end + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + project.team << [user2, :developer] + end + + describe '#execute' do + context 'valid params' do + let(:service) { described_class.new(project, user, {}) } + + before do + allow(service).to receive(:execute_hooks) + + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + it "does not create merge commit" do + source_branch_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha + expect(source_branch_sha).to eq(target_branch_sha) + end + + it { expect(merge_request).to be_valid } + it { expect(merge_request).to be_merged } + + it 'sends email to user2 about merge of new merge_request' do + email = ActionMailer::Base.deliveries.last + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) + end + + it 'creates system note about merge_request merge' do + note = merge_request.notes.last + expect(note.note).to include 'merged' + end + end + + context "error handling" do + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } + + before do + allow(Rails.logger).to receive(:error) + end + + it 'logs and saves error if there is an exception' do + error_message = 'error message' + + allow(service).to receive(:repository).and_raise("error message") + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + + it 'logs and saves error if there is an PreReceiveError exception' do + error_message = 'error message' + + allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message) + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index c2ec805ea99..dc89fdebce7 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -76,9 +76,8 @@ describe Projects::CreateService, '#execute' do context 'wiki_enabled true creates wiki repository directory' do it do project = create_project(user, opts) - path = ProjectWiki.new(project, user).send(:path_to_repo) - expect(File.exist?(path)).to be_truthy + expect(wiki_repo(project).exists?).to be_truthy end end @@ -86,11 +85,15 @@ describe Projects::CreateService, '#execute' do it do opts[:wiki_enabled] = false project = create_project(user, opts) - path = ProjectWiki.new(project, user).send(:path_to_repo) - expect(File.exist?(path)).to be_falsey + expect(wiki_repo(project).exists?).to be_falsey end end + + def wiki_repo(project) + relative_path = ProjectWiki.new(project).disk_path + '.git' + Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar') + end end context 'builds_enabled global setting' do @@ -149,6 +152,9 @@ describe Projects::CreateService, '#execute' do end context 'when another repository already exists on disk' do + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + let(:opts) do { name: 'Existing', @@ -156,31 +162,59 @@ describe Projects::CreateService, '#execute' do } end - let(:repository_storage) { 'default' } - let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + context 'with legacy storage' do + before do + gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") + end - before do - gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") - end + after do + gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + end - after do - gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") - end + it 'does not allow to create a project when path matches existing repository on disk' do + project = create_project(user, opts) - it 'does not allow to create project with same path' do - project = create_project(user, opts) + expect(project).not_to be_persisted + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:base) + expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + end - expect(project).to respond_to(:errors) - expect(project.errors.messages).to have_key(:base) - expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + it 'does not allow to import project when path matches existing repository on disk' do + project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' })) + + expect(project).not_to be_persisted + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:base) + expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + end end - it 'does not allow to import a project with the same path' do - project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' })) + context 'with hashed storage' do + let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } + let(:hashed_path) { '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } + + before do + stub_application_setting(hashed_storage_enabled: true) + allow(Digest::SHA2).to receive(:hexdigest) { hash } + end + + before do + gitlab_shell.add_repository(repository_storage, hashed_path) + end + + after do + gitlab_shell.remove_repository(repository_storage_path, hashed_path) + end + + it 'does not allow to create a project when path matches existing repository on disk' do + project = create_project(user, opts) - expect(project).to respond_to(:errors) - expect(project.errors.messages).to have_key(:base) - expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + expect(project).not_to be_persisted + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:base) + expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + end end end end @@ -209,6 +243,15 @@ describe Projects::CreateService, '#execute' do end end + context 'when skip_disk_validation is used' do + it 'sets the project attribute' do + opts[:skip_disk_validation] = true + project = create_project(user, opts) + + expect(project.skip_disk_validation).to be_truthy + end + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 4873e967535..d400304622e 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -152,22 +152,40 @@ describe Projects::UpdateService, '#execute' do let(:repository_storage) { 'default' } let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } - before do - gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") - end + context 'with legacy storage' do + before do + gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") + end - after do - gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + after do + gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + end + + it 'does not allow renaming when new path matches existing repository on disk' do + result = update_project(project, admin, path: 'existing') + + expect(result).to include(status: :error) + expect(result[:message]).to match('There is already a repository with that name on disk') + expect(project).not_to be_valid + expect(project.errors.messages).to have_key(:base) + expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') + end end - it 'does not allow renaming when new path matches existing repository on disk' do - result = update_project(project, admin, path: 'existing') + context 'with hashed storage' do + let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - expect(result).to include(status: :error) - expect(result[:message]).to match('There is already a repository with that name on disk') - expect(project).not_to be_valid - expect(project.errors.messages).to have_key(:base) - expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') + before do + stub_application_setting(hashed_storage_enabled: true) + end + + it 'does not check if new path matches existing repository on disk' do + expect(project).not_to receive(:repository_with_same_path_already_exists?) + + result = update_project(project, admin, path: 'existing') + + expect(result).to include(status: :success) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dbf05b7f004..576e4ae1d38 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -169,6 +169,24 @@ RSpec.configure do |config| end end +# add simpler way to match asset paths containing digest strings +RSpec::Matchers.define :match_asset_path do |expected| + match do |actual| + path = Regexp.escape(expected) + extname = Regexp.escape(File.extname(expected)) + digest_regex = Regexp.new(path.sub(extname, "(?:-\\h+)?#{extname}") << '$') + digest_regex =~ actual + end + + failure_message do |actual| + "expected that #{actual} would include an asset path for #{expected}" + end + + failure_message_when_negated do |actual| + "expected that #{actual} would not include an asset path for #{expected}" + end +end + FactoryGirl::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..5f22d886910 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -39,11 +39,11 @@ module StubGitlabCalls .and_return({ 'tags' => tags }) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:repository_manifest).with(repository) + .to receive(:repository_manifest).with(repository, anything) .and_return(stub_container_registry_tag_manifest) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:blob).with(repository) + .to receive(:blob).with(repository, anything, 'application/octet-stream') .and_return(stub_container_registry_blob) end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index b4e8b5ea67b..79395f4c564 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -17,6 +17,7 @@ module TestEnv 'feature_conflict' => 'bb5206f', 'fix' => '48f0be4', 'improve/awesome' => '5937ac0', + 'merged-target' => '21751bf', 'markdown' => '0ed8c6c', 'lfs' => 'be93687', 'master' => 'b83d6e3', diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index 1490287681b..50a1d4a56e2 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -25,11 +25,13 @@ shared_examples 'update invalid issuable' do |klass| .and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) end - it 'renders edit when format is html' do - put :update, params + if klass == MergeRequest + it 'renders edit when format is html' do + put :update, params - expect(response).to render_template(:edit) - expect(assigns[:conflict]).to be_truthy + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy + end end it 'renders json error message when format is json' do @@ -42,16 +44,17 @@ shared_examples 'update invalid issuable' do |klass| end end - context 'when updating an invalid issuable' do - before do - key = klass == Issue ? :issue : :merge_request - params[key][:title] = "" - end + if klass == MergeRequest + context 'when updating an invalid issuable' do + before do + params[:merge_request][:title] = "" + end - it 'renders edit when merge request is invalid' do - put :update, params + it 'renders edit when merge request is invalid' do + put :update, params - expect(response).to render_template(:edit) + expect(response).to render_template(:edit) + end end end end diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb deleted file mode 100644 index cf0aa44a4a2..00000000000 --- a/spec/views/projects/registry/repositories/index.html.haml_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe 'projects/registry/repositories/index' do - let(:group) { create(:group, path: 'group') } - let(:project) { create(:project, group: group, path: 'test') } - - let(:repository) do - create(:container_repository, project: project, name: 'image') - end - - before do - stub_container_registry_config(enabled: true, - host_port: 'registry.gitlab', - api_url: 'http://registry.gitlab') - - stub_container_registry_tags(repository: :any, tags: [:latest]) - - assign(:project, project) - assign(:images, [repository]) - - allow(view).to receive(:can?).and_return(true) - end - - it 'contains container repository path' do - render - - expect(rendered).to have_content 'group/test/image' - end - - it 'contains attribute for copying tag location into clipboard' do - render - - expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \ - 'registry.gitlab/group/test/image:latest"]' - end -end diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index d2609d21546..1d9bbf2ca62 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -69,7 +69,12 @@ describe RepositoryCheck::SingleRepositoryWorker do end def break_wiki(project) - FileUtils.rm_rf(wiki_path(project) + '/objects') + objects_dir = wiki_path(project) + '/objects' + + # Replace the /objects directory with a file so that the repo is + # invalid, _and_ 'git init' cannot fix it. + FileUtils.rm_rf(objects_dir) + FileUtils.touch(objects_dir) if File.directory?(wiki_path(project)) end def wiki_path(project) |