diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-12-10 01:27:56 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-12-10 01:27:56 +0800 |
commit | e0df1b5f9944cff1ee7234c9fa292a7a06456c08 (patch) | |
tree | bd3e760fb41f1974bd6aac0ccefbc6f97cdce04f | |
parent | 3e01385bca92dc8c0df3aa4032cc58d708dc0ff5 (diff) | |
parent | ad4c2a08104cba0557d824fac6a70eedd45921b7 (diff) | |
download | gitlab-ce-e0df1b5f9944cff1ee7234c9fa292a7a06456c08.tar.gz |
Merge remote-tracking branch 'upstream/master' into fix-git-hooks-when-creating-file
* upstream/master: (60 commits)
Refactor SSH keys docs
Improvements to setting up ssh
Do not reload diff for merge request made from fork when target branch in fork is updated
Add 8.12.10, 8.12.11, and 8.12.12 CHANGELOG.md items
Changes after review
Fix broken test
Adds CHANGELOG entry
Adds tests
Uniformize props name format
Replace commit icon svg logic
Replace play icon svg logic
Update docs to reflect new defaults on omnibus
Merge branch 'jej-23867-use-mr-finder-instead-of-access-check' into 'security'
Merge branch 'html-safe-diff-line-content' into 'security'
Merge branch 'rs-filter-authentication_token' into 'security'
Merge branch 'destroy-session' into 'security'
remove unnecessary issues event filter on comments tab
Updating reference to database password
Add CHANGELOG entry
Displays milestone remaining days only when it's present
...
173 files changed, 2004 insertions, 758 deletions
diff --git a/.eslintrc b/.eslintrc index b80dcec9d1d..e13f76b213c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ "globals": { "_": false, "gl": false, - "gon": false + "gon": false, + "localStorage": false }, "plugins": [ "filenames" diff --git a/CHANGELOG.md b/CHANGELOG.md index e03123111c3..fb13db4dd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.14.4 (2016-12-08) + +- Fix diff view permalink highlighting. !7090 +- Fix pipeline author for Slack and use pipeline id for pipeline link. !7506 +- Fix compatibility with Internet Explorer 11 for merge requests. !7525 (Steffen Rauh) +- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 +- Fix Cicking on tabs on pipeline page should set URL. !7709 +- Authorize users into imported GitLab project. +- Destroy a user's session when they delete their own account. +- Don't accidentally mark unsafe diff lines as HTML safe. +- Replace MR access checks with use of MergeRequestsFinder. +- Remove visible content caching. + ## 8.14.3 (2016-12-02) - Pass commit data to ProcessCommitWorker to reduce Git overhead. !7744 @@ -251,6 +264,11 @@ entry. - Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix 404 when visit /projects page +## 8.13.9 (2016-12-08) + +- Reenables /user API request to return private-token if user is admin and request is made with sudo. !7615 +- Replace MR access checks with use of MergeRequestsFinder. + ## 8.13.8 (2016-12-02) - Pass tag SHA to post-receive hook when tag is created via UI. !7700 @@ -495,6 +513,21 @@ entry. - Fix broken Project API docs (Takuya Noguchi) - Migrate invalid project members (owner -> master) +## 8.12.12 (2016-12-08) + +- Replace MR access checks with use of MergeRequestsFinder +- Reenables /user API request to return private-token if user is admin and request is made with sudo + +## 8.12.11 (2016-12-02) + +- No changes + +## 8.12.10 (2016-11-28) + +- Fix information disclosure in `Projects::BlobController#update` +- Fix missing access checks on issue lookup using IssuableFinder +- Replace issue access checks with use of IssuableFinder + ## 8.12.9 (2016-11-07) - Fix XSS issue in Markdown autolinker @@ -271,7 +271,7 @@ group :development, :test do gem 'fuubar', '~> 2.0.0' gem 'database_cleaner', '~> 1.5.0' - gem 'factory_girl_rails', '~> 4.6.0' + gem 'factory_girl_rails', '~> 4.7.0' gem 'rspec-rails', '~> 3.5.0' gem 'rspec-retry', '~> 0.4.5' gem 'spinach-rails', '~> 0.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 7a024e81ad2..c464ff70587 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,10 +177,10 @@ GEM excon (0.52.0) execjs (2.6.0) expression_parser (0.9.0) - factory_girl (4.5.0) + factory_girl (4.7.0) activesupport (>= 3.0.0) - factory_girl_rails (4.6.0) - factory_girl (~> 4.5.0) + factory_girl_rails (4.7.0) + factory_girl (~> 4.7.0) railties (>= 3.0.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) @@ -819,7 +819,7 @@ DEPENDENCIES dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) - factory_girl_rails (~> 4.6.0) + factory_girl_rails (~> 4.7.0) ffaker (~> 2.0.0) flay (~> 2.6.1) fog-aws (~> 0.9) diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 5434a19bcec..0ff5c0fab05 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -70,6 +70,8 @@ // e.g. // Api.gitignoreText item.name, @requestFileSuccess.bind(@) requestFileSuccess(file, { skipFocus } = {}) { + if (!file) return; + const oldValue = this.editor.getValue(); let newValue = file.content; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 3a7c5ff3681..413117c2226 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -24,6 +24,7 @@ switch (page) { case 'sessions:new': new UsernameValidator(); + new ActiveTabMemoizer(); break; case 'projects:boards:show': case 'projects:boards:index': diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 84faabf938a..1db29dd47fb 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -74,6 +74,8 @@ projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath, newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, + commitIconSvg: environmentsData.commitIconSvg, + playIconSvg: environmentsData.playIconSvg, }; }, @@ -227,7 +229,9 @@ :model="model" :toggleRow="toggleRow.bind(model)" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed"></tr> + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :commit-icon-svg="commitIconSvg"></tr> <tr v-if="model.isOpen && model.children && model.children.length > 0" is="environment-item" @@ -235,7 +239,9 @@ :model="children" :toggleRow="toggleRow.bind(children)" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed"> + :can-read-environment="canReadEnvironmentParsed" + :play-icon-svg="playIconSvg" + :commit-icon-svg="commitIconSvg"> </tr> </template> diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index d149a446e0b..7c743705d51 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -12,38 +12,18 @@ required: false, default: () => [], }, - }, - - /** - * Appends the svg icon that were render in the index page. - * In order to reuse the svg instead of copy and paste in this template - * we need to render it outside this component using =custom_icon partial. - * - * TODO: Remove this when webpack is merged. - * - */ - mounted() { - const playIcon = document.querySelector('.play-icon-svg.hidden svg'); - - const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); - const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); - // Phantomjs does not have support to iterate a nodelist. - const actionsArray = [].slice.call(actionContainers); - - if (playIcon && actionsArray && dropdownContainer) { - dropdownContainer.appendChild(playIcon.cloneNode(true)); - actionsArray.forEach((element) => { - element.appendChild(playIcon.cloneNode(true)); - }); - } + playIconSvg: { + type: String, + required: false, + }, }, template: ` <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="dropdown-play-icon-container"></span> + <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> <i class="fa fa-caret-down"></i> </a> @@ -53,7 +33,9 @@ data-method="post" rel="nofollow" class="js-manual-action-link"> - <span class="action-play-icon-container"></span> + + <span class="js-action-play-icon-container" v-html="playIconSvg"></span> + <span> {{action.name}} </span> diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index 79cd5ded5bd..aed65b33c04 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -7,14 +7,14 @@ window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { props: { - external_url: { + externalUrl: { type: String, default: '', }, }, template: ` - <a class="btn external_url" :href="external_url" target="_blank"> + <a class="btn external_url" :href="externalUrl" target="_blank"> <i class="fa fa-external-link"></i> </a> `, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 6ed14261fc3..2e046a60146 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -58,6 +58,16 @@ required: false, default: false, }, + + commitIconSvg: { + type: String, + required: false, + }, + + playIconSvg: { + type: String, + required: false, + }, }, data() { @@ -451,11 +461,12 @@ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <commit-component :tag="commitTag" - :commit_ref="commitRef" - :commit_url="commitUrl" - :short_sha="commitShortSha" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" :title="commitTitle" - :author="commitAuthor"> + :author="commitAuthor" + :commit-icon-svg="commitIconSvg"> </commit-component> </div> <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> @@ -476,6 +487,7 @@ <div v-if="hasManualActions && canCreateDeployment" class="inline js-manual-actions-container"> <actions-component + :play-icon-svg="playIconSvg" :actions="manualActions"> </actions-component> </div> @@ -483,22 +495,22 @@ <div v-if="model.external_url && canReadEnvironment" class="inline js-external-url-container"> <external-url-component - :external_url="model.external_url"> - </external_url-component> + :external-url="model.external_url"> + </external-url-component> </div> <div v-if="isStoppable && canCreateDeployment" class="inline js-stop-component-container"> <stop-component - :stop_url="model.stop_path"> + :stop-url="model.stop_path"> </stop-component> </div> <div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container"> <rollback-component - :is_last_deployment="isLastDeployment" - :retry_url="retryUrl"> + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"> </rollback-component> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index 55e5c826e07..6d4e8fad604 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -7,19 +7,20 @@ window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { props: { - retry_url: { + retryUrl: { type: String, default: '', }, - is_last_deployment: { + + isLastDeployment: { type: Boolean, default: true, }, }, template: ` - <a class="btn" :href="retry_url" data-method="post" rel="nofollow"> - <span v-if="is_last_deployment"> + <a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> + <span v-if="isLastDeployment"> Re-deploy </span> <span v-else> diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index e6d66a0148c..7292f924e5c 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -7,7 +7,7 @@ window.gl.environmentsList.StopComponent = Vue.component('stop-component', { props: { - stop_url: { + stopUrl: { type: String, default: '', }, @@ -15,7 +15,7 @@ template: ` <a class="btn stop-env-link" - :href="stop_url" + :href="stopUrl" data-confirm="Are you sure you want to stop this environment?" data-method="post" rel="nofollow"> diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 963d2851e5f..e8e502694d6 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -29,7 +29,7 @@ setTimeago = true; } - $timeagoEls.each(function() { + $timeagoEls.filter(':not([data-timeago-rendered])').each(function() { var $el = $(this); $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); @@ -39,6 +39,8 @@ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' }); } + + $el.attr('data-timeago-rendered', true); gl.utils.renderTimeago($el); }); }; diff --git a/app/assets/javascripts/signin_tabs_memoizer.js.es6 b/app/assets/javascripts/signin_tabs_memoizer.js.es6 new file mode 100644 index 00000000000..d811d1cd53a --- /dev/null +++ b/app/assets/javascripts/signin_tabs_memoizer.js.es6 @@ -0,0 +1,49 @@ +/* eslint no-param-reassign: ["error", { "props": false }]*/ +/* eslint no-new: "off" */ +((global) => { + /** + * Memorize the last selected tab after reloading a page. + * Does that setting the current selected tab in the localStorage + */ + class ActiveTabMemoizer { + constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { + this.currentTabKey = currentTabKey; + this.tabSelector = tabSelector; + this.bootstrap(); + } + + bootstrap() { + const tabs = document.querySelectorAll(this.tabSelector); + if (tabs.length > 0) { + tabs[0].addEventListener('click', (e) => { + if (e.target && e.target.nodeName === 'A') { + const anchorName = e.target.getAttribute('href'); + this.saveData(anchorName); + } + }); + } + + this.showTab(); + } + + showTab() { + const anchorName = this.readData(); + if (anchorName) { + const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); + if (tab) { + tab.click(); + } + } + } + + saveData(val) { + localStorage.setItem(this.currentTabKey, val); + } + + readData() { + return localStorage.getItem(this.currentTabKey); + } + } + + global.ActiveTabMemoizer = ActiveTabMemoizer; +})(window); diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 2ef2959cbf4..62a22e39a3b 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -23,7 +23,7 @@ * name * ref_url */ - commit_ref: { + commitRef: { type: Object, required: false, default: () => ({}), @@ -32,16 +32,16 @@ /** * Used to link to the commit sha. */ - commit_url: { + commitUrl: { type: String, required: false, default: '', }, /** - * Used to show the commit short_sha that links to the commit url. + * Used to show the commit short sha that links to the commit url. */ - short_sha: { + shortSha: { type: String, required: false, default: '', @@ -68,6 +68,11 @@ required: false, default: () => ({}), }, + + commitIconSvg: { + type: String, + required: false, + }, }, computed: { @@ -80,7 +85,7 @@ * @returns {Boolean} */ hasCommitRef() { - return this.commit_ref && this.commit_ref.name && this.commit_ref.ref_url; + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; }, /** @@ -110,24 +115,6 @@ }, }, - /** - * In order to reuse the svg instead of copy and paste in this template - * we need to render it outside this component using =custom_icon partial. - * Make sure it has this structure: - * .commit-icon-svg.hidden - * svg - * - * TODO: Find a better way to include SVG - */ - mounted() { - const commitIconContainer = this.$el.querySelector('.commit-icon-container'); - const commitIcon = document.querySelector('.commit-icon-svg.hidden svg'); - - if (commitIconContainer && commitIcon) { - commitIconContainer.appendChild(commitIcon.cloneNode(true)); - } - }, - template: ` <div class="branch-commit"> @@ -138,15 +125,15 @@ <a v-if="hasCommitRef" class="monospace branch-name" - :href="commit_ref.ref_url"> - {{commit_ref.name}} + :href="commitRef.ref_url"> + {{commitRef.name}} </a> - <div class="icon-container commit-icon commit-icon-container"></div> + <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> <a class="commit-id monospace" - :href="commit_url"> - {{short_sha}} + :href="commitUrl"> + {{shortSha}} </a> <p class="commit-title"> @@ -162,7 +149,7 @@ </a> <a class="commit-row-message" - :href="commit_url"> + :href="commitUrl"> {{title}} </a> </span> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 4d4835568ed..c82a9a2b9e3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -6,6 +6,7 @@ @import "framework/animations.scss"; @import "framework/avatar.scss"; +@import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; @import "framework/calendar.scss"; diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss new file mode 100644 index 00000000000..62493c32833 --- /dev/null +++ b/app/assets/stylesheets/framework/asciidoctor.scss @@ -0,0 +1,27 @@ +.admonitionblock td.icon { + width: 1%; + + [class^="fa icon-"] { + @extend .fa-2x; + } + + .icon-note { + @extend .fa-thumb-tack; + } + + .icon-tip { + @extend .fa-lightbulb-o; + } + + .icon-warning { + @extend .fa-exclamation-triangle; + } + + .icon-caution { + @extend .fa-fire; + } + + .icon-important { + @extend .fa-exclamation-circle; + } +} diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index c13cb4a02b2..dece5c3202b 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -1,7 +1,7 @@ .awards { .emoji-icon { - width: 19px; - height: 19px; + width: 20px; + height: 20px; } } @@ -136,5 +136,6 @@ .award-control-icon { color: $award-emoji-new-btn-icon-color; + margin-top: 1px; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 600bf17259b..251e43d2edd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -255,6 +255,7 @@ img.emoji { height: 20px; vertical-align: top; width: 20px; + margin-top: 1px; } .chart { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 69da520f21f..ea77348633d 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -71,6 +71,10 @@ border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; + + .badge { + color: $black; + } } .badge { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 16b099c09eb..10eb3d4203e 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -124,7 +124,7 @@ ul.notes { position: absolute; left: 0; bottom: 0; - background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%); + background: linear-gradient(rgba($white-light, 0.1) -100px, $white-light 100%); } &.hide-shade { @@ -413,7 +413,6 @@ ul.notes { .fa { color: $notes-action-color; position: relative; - top: 1px; font-size: 17px; } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index aa7570cd896..1e3d194e9f9 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -56,7 +56,7 @@ class Admin::GroupsController < Admin::ApplicationController private def group - @group ||= Group.find_by(path: params[:id]) + @group ||= Group.find_by_full_path(params[:id]) end def group_params diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 5d11f286e9a..d2fcb2efd6a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -82,10 +82,8 @@ module CreatesCommit def merge_request_exists? return @merge_request if defined?(@merge_request) - @merge_request = @mr_target_project.merge_requests.opened.find_by( - source_branch: @mr_source_branch, - target_branch: @mr_target_branch - ) + @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. + find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch) end def different_project? diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 949b4a6c25a..c411c21bb80 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -9,7 +9,7 @@ class Groups::ApplicationController < ApplicationController def group unless @group id = params[:group_id] || params[:id] - @group = Group.find_by(path: id) + @group = Group.find_by_full_path(id) unless @group && can?(current_user, :read_group, @group) @group = nil diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index cdfc1ba7b92..8197d9e4c99 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -65,7 +65,7 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", + create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end @@ -74,26 +74,24 @@ class Projects::CommitController < Projects::ApplicationController return render_404 if @target_branch.blank? - create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", success_path: successful_change_path, failure_path: failed_change_path) end private def successful_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commits_url(@project.namespace, @project, @target_branch) + referenced_merge_request_url || namespace_project_commits_url(@project.namespace, @project, @target_branch) end def failed_change_path - return referenced_merge_request_url if @commit.merged_merge_request - - namespace_project_commit_url(@project.namespace, @project, params[:id]) + referenced_merge_request_url || namespace_project_commit_url(@project.namespace, @project, params[:id]) end def referenced_merge_request_url - namespace_project_merge_request_url(@project.namespace, @project, @commit.merged_merge_request) + if merge_request = @commit.merged_merge_request(current_user) + namespace_project_merge_request_url(@project.namespace, @project, merge_request) + end end def commit diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index aba87b6144b..ad92f05a42d 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -21,7 +21,7 @@ class Projects::CommitsController < Projects::ApplicationController @note_counts = project.notes.where(commit_id: @commits.map(&:id)). group(:commit_id).count - @merge_request = @project.merge_requests.opened. + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index e2b178314c0..325987199fa 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -54,7 +54,7 @@ class Projects::CompareController < Projects::ApplicationController end def merge_request - @merge_request ||= @project.merge_requests.opened. + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened. find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index 148e39630e3..1349b015a63 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -24,7 +24,7 @@ class Projects::DiscussionsController < Projects::ApplicationController private def merge_request - @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id]) + @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id]) end def discussion diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 699a56ae2f8..3fb8bba3cd0 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -10,14 +10,38 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_members = @project.project_members @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + group = @project.group + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + # FIXME: This whole logic should be moved to a finder! + non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) + group_members = group.group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, @group) + end + if params[:search].present? - users = @project.users.search(params[:search]).to_a - @project_members = @project_members.where(user_id: users) + user_ids = @project.users.search(params[:search]).select(:id) + @project_members = @project_members.where(user_id: user_ids) + + if group_members + user_ids = group.users.search(params[:search]).select(:id) + group_members = group_members.where(user_id: user_ids) + end @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - @project_members = @project_members.order(access_level: :desc).page(params[:page]) + member_ids = @project_members.pluck(:id) + + if group_members + member_ids += group_members.pluck(:id) + end + + @project_members = Member.where(id: member_ids).order(access_level: :desc).page(params[:page]) @requesters = AccessRequestsFinder.new(@project).execute(current_user) diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 52517381c65..a41fcb85c40 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -18,7 +18,7 @@ class Projects::TodosController < Projects::ApplicationController when "issue" IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) when "merge_request" - @project.merge_requests.find(params[:issuable_id]) + MergeRequestsFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3327f4f2b87..c45196cc3e9 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -27,7 +27,10 @@ class RegistrationsController < Devise::RegistrationsController DeleteUserService.new(current_user).execute(current_user) respond_to do |format| - format.html { redirect_to new_user_session_path, notice: "Account successfully removed." } + format.html do + session.try(:destroy) + redirect_to new_user_session_path, notice: "Account successfully removed." + end end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5d7ecfeacf4..38e7c6f4a48 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -31,6 +31,8 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end + # hide the signed-in notification + flash[:notice] = nil log_audit_event(current_user, with: authentication_method) end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c9bee01b9ad..b4c14d05eaf 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -77,6 +77,10 @@ class IssuableFinder counts end + def find_by!(*params) + execute.find_by!(*params) + end + def group return @group if defined?(@group) diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index a653a6d59c6..2484339e3a4 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -14,7 +14,7 @@ class NotesFinder when "issue" IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index ed402b698fb..66a720a9426 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -130,7 +130,7 @@ module CommitsHelper def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip + tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip if can_collaborate_with_project? btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? @@ -154,7 +154,7 @@ module CommitsHelper def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" + tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" if can_collaborate_with_project? btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f489f9aa0d6..c35d6611ab0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -55,7 +55,9 @@ module DiffHelper if line.blank? " ".html_safe else - line.sub(/^[\-+ ]/, '').html_safe + # We can't use `sub` because the HTML-safeness of `line` will not survive. + line[0] = '' if line.start_with?('+', '-', ' ') + line end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index f1a0b929d82..362046c0270 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -45,6 +45,12 @@ module EventsHelper @project.feature_available?(feature_key, current_user) end + def comments_visible? + event_filter_visible(:repository) || + event_filter_visible(:merge_requests) || + event_filter_visible(:issues) + end + def event_preposition(event) if event.push? || event.commented? || event.target "at" diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 19ab059aea6..f6d4ea4659a 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -5,7 +5,7 @@ module GroupsHelper def group_icon(group) if group.is_a?(String) - group = Group.find_by(path: group) + group = Group.find_by_full_path(group) end group.try(:avatar_url) || image_path('no_group_avatar.png') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index caf6908505e..fda8228a1e9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,8 +21,6 @@ module Ci after_create :keep_around_commits, unless: :importing? - delegate :stages, to: :statuses - state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -98,17 +96,35 @@ module Ci sha[0...8] end - def self.stages - # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries - CommitStatus.where(pipeline: pluck(:id)).stages - end - def self.total_duration where.not(duration: nil).sum(:duration) end - def stages_with_latest_statuses - statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) + def stages_count + statuses.select(:stage).distinct.count + end + + def stages_name + statuses.order(:stage_idx).distinct. + pluck(:stage, :stage_idx).map(&:first) + end + + def stages + status_sql = statuses.latest.where('stage=sg.stage').status_sql + + stages_query = statuses.group('stage').select(:stage) + .order('max(stage_idx)') + + stages_with_statuses = CommitStatus.from(stages_query, :sg). + pluck('sg.stage', status_sql) + + stages_with_statuses.map do |stage| + Ci::Stage.new(self, name: stage.first, status: stage.last) + end + end + + def artifacts + builds.latest.with_artifacts_not_expired end def project_id diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb new file mode 100644 index 00000000000..d2a37c0a827 --- /dev/null +++ b/app/models/ci/stage.rb @@ -0,0 +1,37 @@ +module Ci + # Currently this is artificial object, constructed dynamically + # We should migrate this object to actual database record in the future + class Stage + include StaticModel + + attr_reader :pipeline, :name + + delegate :project, to: :pipeline + + def initialize(pipeline, name:, status: nil) + @pipeline = pipeline + @name = name + @status = status + end + + def to_param + name + end + + def status + @status ||= statuses.latest.status + end + + def detailed_status + Gitlab::Ci::Status::Stage::Factory.new(self).fabricate! + end + + def statuses + @statuses ||= pipeline.statuses.where(stage: name) + end + + def builds + @builds ||= pipeline.builds.where(stage: name) + end + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb index 248140f421b..1831cc7e175 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -245,44 +245,47 @@ class Commit project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end - def revert_description - if merged_merge_request - "This reverts merge request #{merged_merge_request.to_reference}" + def revert_description(user) + if merged_merge_request?(user) + "This reverts merge request #{merged_merge_request(user).to_reference}" else "This reverts commit #{sha}" end end - def revert_message - %Q{Revert "#{title.strip}"\n\n#{revert_description}} + def revert_message(user) + %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}} end - def reverts_commit?(commit) - description? && description.include?(commit.revert_description) + def reverts_commit?(commit, user) + description? && description.include?(commit.revert_description(user)) end def merge_commit? parents.size > 1 end - def merged_merge_request - return @merged_merge_request if defined?(@merged_merge_request) - - @merged_merge_request = project.merge_requests.find_by(merge_commit_sha: id) if merge_commit? + def merged_merge_request(current_user) + # Memoize with per-user access check + @merged_merge_request_hash ||= Hash.new do |hash, user| + hash[user] = merged_merge_request_no_cache(user) + end + + @merged_merge_request_hash[current_user] end - def has_been_reverted?(current_user = nil, noteable = self) + def has_been_reverted?(current_user, noteable = self) ext = all_references(current_user) noteable.notes_with_associations.system.each do |note| note.all_references(current_user, extractor: ext) end - ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) } end - def change_type_title - merged_merge_request ? 'merge request' : 'commit' + def change_type_title(user) + merged_merge_request?(user) ? 'merge request' : 'commit' end # Get the URI type of the given path @@ -350,4 +353,12 @@ class Commit changes end + + def merged_merge_request?(user) + !!merged_merge_request(user) + end + + def merged_merge_request_no_cache(user) + MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? + end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c345bf293c9..cf90475f4d4 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -31,18 +31,13 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - quoted_when = connection.quote_column_name('when') # We want to ignore failed_but_allowed jobs where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]). - # We want to ignore skipped manual jobs - where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped'). - # We want to ignore skipped on_failure - where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped') + false, all_state_names - [:failed, :canceled]) end - scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) } - scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) } + scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } + scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } state_machine :status do event :enqueue do @@ -117,20 +112,6 @@ class CommitStatus < ActiveRecord::Base name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip end - def self.stages - # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') - end - - def self.stages_status - # We execute subquery for each stage to calculate a stage status - statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) - statuses.inject({}) do |h, k| - h[k.first] = k.last - h - end - end - def failed_but_allowed? allow_failure? && (failed? || canceled?) end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 2f5aa91a964..90432fc4050 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -4,7 +4,7 @@ module HasStatus AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] - COMPLETED_STATUSES = %w[success failed canceled] + COMPLETED_STATUSES = %w[success failed canceled skipped] ORDERED_STATUSES = %w[failed pending running canceled success skipped] class_methods do @@ -23,9 +23,10 @@ module HasStatus canceled = scope.canceled.select('count(*)').to_sql "(CASE + WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' - WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped' + WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index e65fc9eaa09..875e9834487 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,17 +1,17 @@ module Milestoneish - def closed_items_count(user = nil) + def closed_items_count(user) issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size end - def total_items_count(user = nil) + def total_items_count(user) issues_visible_to_user(user).size + merge_requests.size end - def complete?(user = nil) + def complete?(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end - def percent_complete(user = nil) + def percent_complete(user) ((closed_items_count(user) * 100) / total_items_count(user)).abs rescue ZeroDivisionError 0 @@ -29,7 +29,7 @@ module Milestoneish (Date.today - start_date).to_i end - def issues_visible_to_user(user = nil) + def issues_visible_to_user(user) issues.visible_to_user(user) end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb new file mode 100644 index 00000000000..d36bb9da296 --- /dev/null +++ b/app/models/concerns/routable.rb @@ -0,0 +1,70 @@ +# Store object full path in separate table for easy lookup and uniq validation +# Object must have path db field and respond to full_path and full_path_changed? methods. +module Routable + extend ActiveSupport::Concern + + included do + has_one :route, as: :source, autosave: true, dependent: :destroy + + validates_associated :route + + before_validation :update_route_path, if: :full_path_changed? + end + + class_methods do + # Finds a single object by full path match in routes table. + # + # Usage: + # + # Klass.find_by_full_path('gitlab-org/gitlab-ce') + # + # Returns a single object, or nil. + def find_by_full_path(path) + # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so + # any literal matches come first, for this we have to use "BINARY". + # Without this there's still no guarantee in what order MySQL will return + # rows. + binary = Gitlab::Database.mysql? ? 'BINARY' : '' + + order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" + + where_paths_in([path]).reorder(order_sql).take + end + + # Builds a relation to find multiple objects by their full paths. + # + # Usage: + # + # Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # + # Returns an ActiveRecord::Relation. + def where_paths_in(paths) + wheres = [] + cast_lower = Gitlab::Database.postgresql? + + paths.each do |path| + path = connection.quote(path) + where = "(routes.path = #{path})" + + if cast_lower + where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" + end + + wheres << where + end + + if wheres.empty? + none + else + joins(:route).where(wheres.join(' OR ')) + end + end + end + + private + + def update_route_path + route || build_route(source: self) + route.path = full_path + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 33b578e12c1..ea3cf1cdaac 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -101,7 +101,9 @@ class MergeRequest < ActiveRecord::Base validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? - scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } + scope :by_source_or_target_branch, ->(branch_name) do + where("source_branch = :branch OR target_branch = :branch", branch: branch_name) + end scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :of_projects, ->(ids) { where(target_project_id: ids) } @@ -805,7 +807,7 @@ class MergeRequest < ActiveRecord::Base @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end - def can_be_reverted?(current_user = nil) + def can_be_reverted?(current_user) merge_commit && !merge_commit.has_been_reverted?(current_user, self) end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7a545f752b6..37374044551 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -4,12 +4,16 @@ class Namespace < ActiveRecord::Base include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + include Routable cache_markdown_field :description, pipeline: :description has_many :projects, dependent: :destroy belongs_to :owner, class_name: "User" + belongs_to :parent, class_name: "Namespace" + has_many :children, class_name: "Namespace", foreign_key: :parent_id + validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, presence: true, @@ -86,7 +90,7 @@ class Namespace < ActiveRecord::Base end def to_param - path + full_path end def human_name @@ -150,6 +154,14 @@ class Namespace < ActiveRecord::Base Gitlab.config.lfs.enabled end + def full_path + if parent + parent.full_path + '/' + path + else + path + end + end + private def repository_storage_paths @@ -185,4 +197,8 @@ class Namespace < ActiveRecord::Base where(projects: { namespace_id: id }). find_each(&:refresh_members_authorized_projects) end + + def full_path_changed? + path_changed? || parent_id_changed? + end end diff --git a/app/models/project.rb b/app/models/project.rb index 590885c0177..77d740081c6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,6 +14,7 @@ class Project < ActiveRecord::Base include TokenAuthenticatable include ProjectFeaturesCompatibility include SelectForProjectAuthorization + include Routable extend Gitlab::ConfigHelper @@ -324,87 +325,6 @@ class Project < ActiveRecord::Base non_archived.where(table[:name].matches(pattern)) end - # Finds a single project for the given path. - # - # path - The full project path (including namespace path). - # - # Returns a Project, or nil if no project could be found. - def find_with_namespace(path) - namespace_path, project_path = path.split('/', 2) - - return unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so - # any literal matches come first, for this we have to use "BINARY". - # Without this there's still no guarantee in what order MySQL will return - # rows. - binary = Gitlab::Database.mysql? ? 'BINARY' : '' - - order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \ - "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)" - - where_paths_in([path]).reorder(order_sql).take - end - - # Builds a relation to find multiple projects by their full paths. - # - # Each path must be in the following format: - # - # namespace_path/project_path - # - # For example: - # - # gitlab-org/gitlab-ce - # - # Usage: - # - # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) - # - # This would return the projects with the full paths matching the values - # given. - # - # paths - An Array of full paths (namespace path + project path) for which - # to find the projects. - # - # Returns an ActiveRecord::Relation. - def where_paths_in(paths) - wheres = [] - cast_lower = Gitlab::Database.postgresql? - - paths.each do |path| - namespace_path, project_path = path.split('/', 2) - - next unless namespace_path && project_path - - namespace_path = connection.quote(namespace_path) - project_path = connection.quote(project_path) - - where = "(namespaces.path = #{namespace_path} - AND projects.path = #{project_path})" - - if cast_lower - where = "( - #{where} - OR ( - LOWER(namespaces.path) = LOWER(#{namespace_path}) - AND LOWER(projects.path) = LOWER(#{project_path}) - ) - )" - end - - wheres << where - end - - if wheres.empty? - none - else - joins(:namespace).where(wheres.join(' OR ')) - end - end - def visibility_levels Gitlab::VisibilityLevel.options end @@ -440,6 +360,10 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end + + # Add alias for Routable method for compatibility with old code. + # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` + alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -879,13 +803,14 @@ class Project < ActiveRecord::Base end alias_method :human_name, :name_with_namespace - def path_with_namespace - if namespace - namespace.path + '/' + path + def full_path + if namespace && path + namespace.full_path + '/' + path else path end end + alias_method :path_with_namespace, :full_path def execute_hooks(data, hooks_scope = :push_hooks) hooks.send(hooks_scope).each do |hook| @@ -1373,4 +1298,8 @@ class Project < ActiveRecord::Base def validate_board_limit(board) raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS end + + def full_path_changed? + path_changed? || namespace_id_changed? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 4034a49ae63..44f66b89600 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -940,7 +940,7 @@ class Repository committer = user_to_committer(user) Rugged::Commit.create(rugged, - message: commit.revert_message, + message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, diff --git a/app/models/route.rb b/app/models/route.rb new file mode 100644 index 00000000000..d40214b9da6 --- /dev/null +++ b/app/models/route.rb @@ -0,0 +1,22 @@ +class Route < ActiveRecord::Base + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + + after_update :rename_children, if: :path_changed? + + def rename_children + # We update each row separately because MySQL does not have regexp_replace. + # rubocop:disable Rails/FindEach + Route.where('path LIKE ?', "#{path_was}%").each do |route| + # Note that update column skips validation and callbacks. + # We need this to avoid recursive call of rename_children method + route.update_column(:path, route.path.sub(path_was, path)) + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 2e028c44d8b..79eb97b7b55 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -44,11 +44,11 @@ module Ci def valid_statuses_for_when(value) case value when 'on_success' - %w[success] + %w[success skipped] when 'on_failure' %w[failed] when 'always' - %w[success failed] + %w[success failed skipped] else [] end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index d49fcd42a08..99d459908e7 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -41,7 +41,7 @@ module Commits success else - error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. + error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content." raise ChangeError, error_msg end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index a880952e274..2316c57bf1e 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -20,6 +20,10 @@ class DestroyGroupService ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end + group.children.each do |group| + DestroyGroupService.new(group, current_user).async_execute + end + group.really_destroy! end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e4056306bc4..0a9563ed7e7 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -55,8 +55,9 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too def reload_merge_requests - merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a - merge_requests += fork_merge_requests.by_branch(@branch_name).to_a + merge_requests = @project.merge_requests.opened. + by_source_or_target_branch(@branch_name).to_a + merge_requests += fork_merge_requests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| @@ -157,13 +158,14 @@ module MergeRequests def merge_requests_for_source_branch @source_merge_requests ||= begin merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a + merge_requests += fork_merge_requests filter_merge_requests(merge_requests) end end def fork_merge_requests - @fork_merge_requests ||= @project.fork_merge_requests.opened + @fork_merge_requests ||= @project.fork_merge_requests.opened. + where(source_branch: @branch_name).to_a end def branch_added? diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index b10ad71d052..fbaea2744a3 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff] + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg] # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index fa8e7979461..af87129e49e 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,4 +1,5 @@ - page_title "Sign in" + %div - if form_based_providers.any? = render 'devise/shared/tabs_ldap' diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 701bcd3ab71..7bd11f5727a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -77,7 +77,7 @@ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 697c8d19257..56c1949ab2b 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -133,7 +133,7 @@ %tr.success-message %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"} - build_count = @pipeline.statuses.latest.size - - stage_count = @pipeline.stages.size + - stage_count = @pipeline.stages_count Pipeline %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = "\##{@pipeline.id}" diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index ae22d474f2c..40e5e306426 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -16,7 +16,7 @@ Commit Author: <%= commit.author_name %> <% end -%> <% build_count = @pipeline.statuses.latest.size -%> -<% stage_count = @pipeline.stages.size -%> +<% stage_count = @pipeline.stages_count -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml new file mode 100644 index 00000000000..afe2fd7fd7b --- /dev/null +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -0,0 +1,15 @@ +- form = local_assigns.fetch(:form) + +.form-group + .checkbox.builds-feature + = form.label :only_allow_merge_if_build_succeeds do + = form.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + %br + %span.descr + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + .checkbox + = form.label :only_allow_merge_if_all_discussions_are_resolved do + = form.check_box :only_allow_merge_if_all_discussions_are_resolved + %strong Only allow merge requests to be merged if all discussions are resolved diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 6e143c4b570..818010bc7d3 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,8 @@ -.merge-requests-feature - %fieldset.builds-feature - %hr - %h5.prepend-top-0 - Merge Requests - .form-group - .checkbox - = f.label :only_allow_merge_if_build_succeeds do - = f.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds - %br - %span.descr - Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') - .checkbox - = f.label :only_allow_merge_if_all_discussions_are_resolved do - = f.check_box :only_allow_merge_if_all_discussions_are_resolved - %strong Only allow merge requests to be merged if all discussions are resolved +- form = local_assigns.fetch(:form) + +%fieldset.features.merge-requests-feature.append-bottom-default + %hr + %h5.prepend-top-0 + Merge Requests + + = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index d5004f6a066..ce8b66b1945 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -111,7 +111,7 @@ %span.label.label-primary = tag - - if @build.pipeline.stages.many? + - if @build.pipeline.stages_count > 1 .dropdown.build-dropdown .title Stage %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} @@ -120,7 +120,7 @@ %ul.dropdown-menu - @build.pipeline.stages.each do |stage| %li - %a.stage-item= stage + %a.stage-item= stage.name .builds-container - HasStatus::ORDERED_STATUSES.each do |build_status| diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e75547c815f..18b3b04154f 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -104,9 +104,9 @@ = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') - elsif allow_retry - - if build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - = icon('repeat') - - elsif build.playable? && !admin + - if build.playable? && !admin = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = custom_icon('icon_play') + - elsif build.retryable? + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + = icon('repeat') diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 0f08f4e8592..b58dceb58c9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,15 +43,13 @@ - else Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.latest.stages_status %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" - - if status + - pipeline.stages.each do |stage| + - if stage.status + - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - = ci_icon_for_status(status) + = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do + = ci_icon_for_status(stage.status) %td - if pipeline.duration diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index e4cd55b9f7a..f6e3d5e76f5 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -11,7 +11,7 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title== #{label} this #{commit.change_type_title} + %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do .form-group.branch diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml deleted file mode 100644 index 3a3d750439f..00000000000 --- a/app/views/projects/commit/_ci_stage.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%tr - %th{colspan: 10} - %strong - %a{name: stage} - - status = statuses.latest.status - %span{class: "ci-status-link ci-status-icon-#{status}"} - = ci_icon_for_status(status) - - if stage - - = stage.titleize - = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true - = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true -%tr - %td{colspan: 10} - diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 1174158eb65..c7b5c1124b3 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -24,20 +24,8 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.pipeline-graph.hidden - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses - + .row-content-block.build-content.middle-block.hidden + = render "projects/pipelines/graph", pipeline: pipeline - if pipeline.yaml_errors.present? .bs-callout.bs-callout-danger @@ -62,5 +50,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml deleted file mode 100644 index f9a9c8707f5..00000000000 --- a/app/views/projects/commit/_pipeline_stage.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- status_groups = statuses.sort_by(&:name).group_by(&:group_name) -- status_groups.each do |group_name, grouped_statuses| - - if grouped_statuses.one? - - status = grouped_statuses.first - - is_playable = status.playable? && can?(current_user, :update_build, @project) - %li.build{ class: ("playable" if is_playable) } - .curve - .build-content - = render "projects/#{status.to_partial_path}_pipeline", subject: status - - else - %li.build - .curve - .dropdown.inline.build-content - = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 2dc91a9b762..7f42fde0fea 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -12,4 +12,4 @@ %th Stages %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false + = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3a5af2723c6..01cd8fa0938 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -112,7 +112,8 @@ %span.descr Enable Container Registry for this project = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - = render 'merge_request_settings', f: f + = render 'merge_request_settings', form: f + %hr %fieldset.features.append-bottom-default %h5.prepend-top-0 diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a9235d6af35..a65a630f2d0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -17,4 +17,6 @@ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "help-page-path" => help_page_path("ci/environments"), - "css-class" => container_class}} + "css-class" => container_class, + "commit-icon-svg" => custom_icon("icon_commit"), + "play-icon-svg" => custom_icon("icon_play")}} diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 3d0ab5b85d6..98d81308407 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -13,7 +13,11 @@ - if @forked_project && @forked_project.errors.any? %p – - = @forked_project.errors.full_messages.first + - error = @forked_project.errors.full_messages.first + - if error.include?("already been taken") + Name has already been taken + - else + = error %p = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 0e2975bd551..896f10104fa 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -31,7 +31,7 @@ %span.label-branch= source_branch_with_namespace(@merge_request) %span into %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) diff --git a/app/views/projects/pipelines/_graph.html.haml b/app/views/projects/pipelines/_graph.html.haml new file mode 100644 index 00000000000..0202833c0bf --- /dev/null +++ b/app/views/projects/pipelines/_graph.html.haml @@ -0,0 +1,4 @@ +- pipeline = local_assigns.fetch(:pipeline) +.pipeline-visualization.pipeline-graph + %ul.stage-column-list + = render partial: "projects/stage/graph", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 3464e155a1b..739e5930822 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -12,19 +12,8 @@ .tab-content #js-tab-pipeline.tab-pane - .build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + .build-content.middle-block + = render "projects/pipelines/graph", pipeline: pipeline #js-tab-builds.tab-pane - if pipeline.yaml_errors.present? @@ -50,5 +39,4 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4bc49072f35..e1e787dbde4 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -37,7 +37,6 @@ %span CI Lint %div.content-list.pipelines - - stages = @pipelines.stages - if @pipelines.blank? %div .nothing-here-block No pipelines to show @@ -51,6 +50,6 @@ %th Stages %th %th.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + = render @pipelines, commit_sha: true, stage: true, allow_retry: true = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml new file mode 100644 index 00000000000..1d8fa10db0c --- /dev/null +++ b/app/views/projects/stage/_graph.html.haml @@ -0,0 +1,22 @@ +- stage = local_assigns.fetch(:stage) +- statuses = stage.statuses.latest +- status_groups = statuses.sort_by(&:name).group_by(&:group_name) +%li.stage-column + .stage-name + %a{ name: stage.name } + = stage.name.titleize + .builds-container + %ul + - status_groups.each do |group_name, grouped_statuses| + - if grouped_statuses.one? + - status = grouped_statuses.first + - is_playable = status.playable? && can?(current_user, :update_build, @project) + %li.build{ class: ("playable" if is_playable) } + .curve + .build-content + = render "projects/#{status.to_partial_path}_pipeline", subject: status + - else + %li.build + .curve + .dropdown.inline.build-content + = render "projects/stage/in_stage_group", name: group_name, subject: grouped_statuses diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 2b26ad9d6fa..2b26ad9d6fa 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml diff --git a/app/views/projects/stage/_stage.html.haml b/app/views/projects/stage/_stage.html.haml new file mode 100644 index 00000000000..1684e02fbad --- /dev/null +++ b/app/views/projects/stage/_stage.html.haml @@ -0,0 +1,13 @@ +%tr + %th{colspan: 10} + %strong + %a{ name: stage.name } + %span{class: "ci-status-link ci-status-icon-#{stage.status}"} + = ci_icon_for_status(stage.status) + + = stage.name.titleize += render stage.statuses.latest_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true += render stage.statuses.retried_ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true +%tr + %td{colspan: 10} + diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c367ae336db..e50ab5fea09 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -5,5 +5,7 @@ - if event_filter_visible(:merge_requests) = event_filter_link EventFilter.merged, 'Merge events' - if event_filter_visible(:issues) + = event_filter_link EventFilter.issue, 'Issue events' + - if comments_visible? = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 432047a1c4e..e67f7d5a352 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -20,8 +20,8 @@ %strong Blocked - if source.instance_of?(Group) && !@group - = link_to source, class: "member-group-link prepend-left-5" do - = "· #{source.name}" + · + = link_to source.name, source, class: "member-group-link" .hidden-xs.cgray - if member.request? @@ -45,7 +45,7 @@ = time_ago_with_tooltip(member.created_at) - if show_roles .controls.member-controls - - if show_controls + - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project) - if user != current_user = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control member-form-control append-right-5 js-member-update-control', id: "member_access_level_#{member.id}", disabled: !can_admin_member diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index 0a237136959..d27fba805a3 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -25,8 +25,10 @@ %span.milestone-stat %strong== #{milestone.percent_complete(current_user)}% complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(milestone) + - remaining_days = milestone_remaining_days(milestone) + - if remaining_days.present? + %span.milestone-stat + %span.remaining-days= remaining_days .milestone-progress-buttons %span.tab-issues-buttons diff --git a/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml new file mode 100644 index 00000000000..19c76b5b437 --- /dev/null +++ b/changelogs/unreleased/15081-wrong-login-tab-ldap-frontend.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong tab selected when loggin fails and multiple login tabs exists +merge_request: 7314 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml b/changelogs/unreleased/23696-fix-diff-view-highlighting.yml deleted file mode 100644 index db523caffed..00000000000 --- a/changelogs/unreleased/23696-fix-diff-view-highlighting.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix diff view permalink highlighting -merge_request: 7090 -author: diff --git a/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml new file mode 100644 index 00000000000..34999480d4a --- /dev/null +++ b/changelogs/unreleased/24507_remove_deleted_branch_link_in_merge_request.yml @@ -0,0 +1,4 @@ +--- +title: 'Remove unnecessary target branch link from MR page in case of deleted target branch' +merge_request: 7916 +author: Rydkin Maxim diff --git a/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml b/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml deleted file mode 100644 index 9fbbaeb914d..00000000000 --- a/changelogs/unreleased/24537-reenable-private-token-with-sudo.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reenables /user API request to return private-token if user is admin and request - is made with sudo -merge_request: 7615 -author: diff --git a/changelogs/unreleased/24814-pipeline-tabs.yml b/changelogs/unreleased/24814-pipeline-tabs.yml deleted file mode 100644 index f85e7576905..00000000000 --- a/changelogs/unreleased/24814-pipeline-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Cicking on tabs on pipeline page should set URL -merge_request: 7709 -author: diff --git a/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml new file mode 100644 index 00000000000..12ea08e3815 --- /dev/null +++ b/changelogs/unreleased/24982-ux-improvement-sign-in-success-message.yml @@ -0,0 +1,5 @@ +--- +title: 'fix: 24982- Remove''Signed in successfully'' message After this change the + sign-in-success flash message will not be shown' +merge_request: 7837 +author: jnoortheen diff --git a/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml new file mode 100644 index 00000000000..a7576e2cbdb --- /dev/null +++ b/changelogs/unreleased/25171-fix-mr-features-settings-hidden-when-builds-are-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Remove wrong '.builds-feature' class from the MR settings fieldset +merge_request: 7930 +author: diff --git a/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml new file mode 100644 index 00000000000..79cb2c6d843 --- /dev/null +++ b/changelogs/unreleased/25272_fix_comments_tab_disappearing.yml @@ -0,0 +1,4 @@ +--- +title: 'Fix comments activity tab visibility condition' +merge_request: 7913 +author: Rydkin Maxim diff --git a/changelogs/unreleased/25374-svg-as-prop.yml b/changelogs/unreleased/25374-svg-as-prop.yml new file mode 100644 index 00000000000..45a71b55b3b --- /dev/null +++ b/changelogs/unreleased/25374-svg-as-prop.yml @@ -0,0 +1,4 @@ +--- +title: Resolve "Provide SVG as a prop instead of hiding and copy them in environments table" +merge_request: 7992 +author: diff --git a/changelogs/unreleased/api-remove-source-branch.yml b/changelogs/unreleased/api-remove-source-branch.yml new file mode 100644 index 00000000000..d1b6507aedb --- /dev/null +++ b/changelogs/unreleased/api-remove-source-branch.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Ability to set ''should_remove_source_branch'' on merge requests' +merge_request: +author: Robert Schilling diff --git a/changelogs/unreleased/destroy-session.yml b/changelogs/unreleased/destroy-session.yml new file mode 100644 index 00000000000..e713e2dc424 --- /dev/null +++ b/changelogs/unreleased/destroy-session.yml @@ -0,0 +1,4 @@ +--- +title: Destroy a user's session when they delete their own account +merge_request: +author: diff --git a/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml new file mode 100644 index 00000000000..12b1460f388 --- /dev/null +++ b/changelogs/unreleased/do-not-refresh-main-when-fork-target-branch-updated.yml @@ -0,0 +1,4 @@ +--- +title: Do not reload diff for merge request made from fork when target branch in fork is updated +merge_request: 7973 +author: diff --git a/changelogs/unreleased/dz-nested-groups.yml b/changelogs/unreleased/dz-nested-groups.yml new file mode 100644 index 00000000000..c227c5a8ea5 --- /dev/null +++ b/changelogs/unreleased/dz-nested-groups.yml @@ -0,0 +1,4 @@ +--- +title: Add nested groups support on data level +merge_request: +author: diff --git a/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml new file mode 100644 index 00000000000..9c52e53c3b4 --- /dev/null +++ b/changelogs/unreleased/enable-asciidoctor-admonition-icons.yml @@ -0,0 +1,4 @@ +--- +title: Enable AsciiDoctor admonition icons +merge_request: 7812 +author: Horacio Sanson diff --git a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml b/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml deleted file mode 100644 index 9f14463fdd1..00000000000 --- a/changelogs/unreleased/fix-authorize-users-into-imported-gitlab-project.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Authorize users into imported GitLab project -merge_request: -author: diff --git a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml b/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml deleted file mode 100644 index db92e45d8f1..00000000000 --- a/changelogs/unreleased/fix-compatibility-with-ie11-for-merge-requests.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix compatibility with Internet Explorer 11 for merge requests -merge_request: 7525 -author: Steffen Rauh diff --git a/changelogs/unreleased/fix-milestone-summary.yml b/changelogs/unreleased/fix-milestone-summary.yml new file mode 100644 index 00000000000..3045a15054c --- /dev/null +++ b/changelogs/unreleased/fix-milestone-summary.yml @@ -0,0 +1,4 @@ +--- +title: Displays milestone remaining days only when it's present +merge_request: +author: diff --git a/changelogs/unreleased/fix-slack-pipeline-event.yml b/changelogs/unreleased/fix-slack-pipeline-event.yml deleted file mode 100644 index fec864eeb3d..00000000000 --- a/changelogs/unreleased/fix-slack-pipeline-event.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline author for Slack and use pipeline id for pipeline link -merge_request: 7506 -author: diff --git a/changelogs/unreleased/group-members-in-project-members-view.yml b/changelogs/unreleased/group-members-in-project-members-view.yml new file mode 100644 index 00000000000..415e2b6b1e2 --- /dev/null +++ b/changelogs/unreleased/group-members-in-project-members-view.yml @@ -0,0 +1,4 @@ +--- +title: Shows group members in project members list +merge_request: +author: diff --git a/changelogs/unreleased/html-safe-diff-line-content.yml b/changelogs/unreleased/html-safe-diff-line-content.yml new file mode 100644 index 00000000000..8f8bbc51963 --- /dev/null +++ b/changelogs/unreleased/html-safe-diff-line-content.yml @@ -0,0 +1,4 @@ +--- +title: Don't accidentally mark unsafe diff lines as HTML safe +merge_request: +author: diff --git a/changelogs/unreleased/issue-events-filter.yml b/changelogs/unreleased/issue-events-filter.yml new file mode 100644 index 00000000000..a3b08bde6e7 --- /dev/null +++ b/changelogs/unreleased/issue-events-filter.yml @@ -0,0 +1,4 @@ +--- +title: Add issue events filter and make all really show all events +merge_request: 7673 +author: Oxan van Leeuwen diff --git a/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml new file mode 100644 index 00000000000..5a4a44b9562 --- /dev/null +++ b/changelogs/unreleased/jej-23867-use-mr-finder-instead-of-access-check.yml @@ -0,0 +1,4 @@ +--- +title: Replace MR access checks with use of MergeRequestsFinder +merge_request: +author: diff --git a/changelogs/unreleased/remove-has-visible-content-caching.yml b/changelogs/unreleased/remove-has-visible-content-caching.yml deleted file mode 100644 index e2940c60443..00000000000 --- a/changelogs/unreleased/remove-has-visible-content-caching.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove visible content caching -merge_request: -author: diff --git a/changelogs/unreleased/render-svg-in-diffs-and-notes.yml b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml new file mode 100644 index 00000000000..827b0dbb1d3 --- /dev/null +++ b/changelogs/unreleased/render-svg-in-diffs-and-notes.yml @@ -0,0 +1,4 @@ +--- +title: Render SVG images in diffs and notes +merge_request: 7747 +author: andrebsguedes diff --git a/changelogs/unreleased/small-emoji-adjustments.yml b/changelogs/unreleased/small-emoji-adjustments.yml new file mode 100644 index 00000000000..804bd05b613 --- /dev/null +++ b/changelogs/unreleased/small-emoji-adjustments.yml @@ -0,0 +1,4 @@ +--- +title: Various small emoji positioning adjustments +merge_request: +author: diff --git a/changelogs/unreleased/timeago-perf-fix.yml b/changelogs/unreleased/timeago-perf-fix.yml new file mode 100644 index 00000000000..265e7db29a9 --- /dev/null +++ b/changelogs/unreleased/timeago-perf-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixed timeago re-rendering every timeago +merge_request: +author: diff --git a/config/application.rb b/config/application.rb index fb84870dfbd..0aa2873f94a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,7 +45,7 @@ module Gitlab # # Parameters filtered: # - Password (:password, :password_confirmation) - # - Private tokens (:private_token) + # - Private tokens (:private_token, :authentication_token) # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) # - Build variables (:variables) @@ -55,6 +55,7 @@ module Gitlab # - Sentry DSN (:sentry_dsn) # - Deploy keys (:key) config.filter_parameters += %i( + authentication_token certificate encrypted_key hook diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 4f30d1265c8..6b0cff75653 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -15,7 +15,7 @@ if Rails.env.production? Raven.configure do |config| config.dsn = current_application_settings.sentry_dsn config.release = Gitlab::REVISION - + # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) # Sanitize authentication headers diff --git a/db/migrate/20161124111390_add_parent_id_to_namespace.rb b/db/migrate/20161124111390_add_parent_id_to_namespace.rb new file mode 100644 index 00000000000..a6fa1b70a9d --- /dev/null +++ b/db/migrate/20161124111390_add_parent_id_to_namespace.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddParentIdToNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column(:namespaces, :parent_id, :integer) + end +end diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb new file mode 100644 index 00000000000..eab74c01dfd --- /dev/null +++ b/db/migrate/20161124111395_add_index_to_parent_id.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToParentId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:namespaces, [:parent_id, :id], unique: true) + end +end diff --git a/db/migrate/20161124111402_add_routes_table.rb b/db/migrate/20161124111402_add_routes_table.rb new file mode 100644 index 00000000000..a02e046a18e --- /dev/null +++ b/db/migrate/20161124111402_add_routes_table.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :routes do |t| + t.integer :source_id, null: false + t.string :source_type, null: false + t.string :path, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb new file mode 100644 index 00000000000..6754e583000 --- /dev/null +++ b/db/migrate/20161130095245_fill_routes_table.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'No new namespaces should be created during data copy' + + def up + execute <<-EOF + INSERT INTO routes + (source_id, source_type, path) + (SELECT id, 'Namespace', path FROM namespaces) + EOF + end + + def down + Route.delete_all(source_type: 'Namespace') + end +end diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb new file mode 100644 index 00000000000..14700583be5 --- /dev/null +++ b/db/migrate/20161130101252_fill_projects_routes_table.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FillProjectsRoutesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = 'No new projects should be created during data copy' + + def up + execute <<-EOF + INSERT INTO routes + (source_id, source_type, path) + (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects + INNER JOIN namespaces ON projects.namespace_id = namespaces.id) + EOF + end + + def down + Route.delete_all(source_type: 'Project') + end +end diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb new file mode 100644 index 00000000000..510796e05f2 --- /dev/null +++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb @@ -0,0 +1,28 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDuplicatesFromRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| + path = connection.quote(row['path']) + execute(%Q{ + DELETE FROM #{quote_table_name(:routes)} + WHERE path = #{path} + AND id != ( + SELECT id FROM ( + SELECT max(id) AS id + FROM #{quote_table_name(:routes)} + WHERE path = #{path} + ) max_ids + ) + }) + end + end + + def down + end +end diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb new file mode 100644 index 00000000000..4a51337bda6 --- /dev/null +++ b/db/migrate/20161202152035_add_index_to_routes.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToRoutes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:routes, :path, unique: true) + add_concurrent_index(:routes, [:source_type, :source_id], unique: true) + end +end diff --git a/db/schema.rb b/db/schema.rb index 0d510c8a269..9c46f573719 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: 20161128161412) do +ActiveRecord::Schema.define(version: 20161202152035) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true end @@ -737,8 +737,9 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" + t.integer "parent_id" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree @@ -746,6 +747,7 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree + add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree @@ -991,6 +993,17 @@ ActiveRecord::Schema.define(version: 20161128161412) do add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree + create_table "routes", force: :cascade do |t| + t.integer "source_id", null: false + t.string "source_type", null: false + t.string "path", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree + add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree + create_table "sent_notifications", force: :cascade do |t| t.integer "project_id" t.integer "noteable_id" @@ -1206,8 +1219,8 @@ ActiveRecord::Schema.define(version: 20161128161412) do t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization" t.string "incoming_email_token" + t.string "organization" t.boolean "authorized_projects_populated" end diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md index 76f3a0fb387..b36cf18d459 100644 --- a/doc/administration/high_availability/database.md +++ b/doc/administration/high_availability/database.md @@ -41,7 +41,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: mailroom['enable'] = false # PostgreSQL configuration - postgresql['sql_password'] = 'DB password' + gitlab_rails['db_password'] = 'DB password' postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0'] postgresql['listen_address'] = '0.0.0.0' ``` @@ -80,7 +80,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL: 1. Similarly, set the password for the `gitlab` database user. Use the same password that you specified in the `/etc/gitlab/gitlab.rb` file for - `postgresql['sql_password']`. + `gitlab_rails['db_password']`. ``` \password gitlab diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 9460b3f73b1..81df55ab4ab 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -271,17 +271,18 @@ Creates a new merge request. POST /projects/:id/merge_requests ``` -Parameters: - -- `id` (required) - The ID of a project -- `source_branch` (required) - The source branch -- `target_branch` (required) - The target branch -- `assignee_id` (optional) - Assignee user ID -- `title` (required) - Title of MR -- `description` (optional) - Description of MR -- `target_project_id` (optional) - The target project (numeric id) -- `labels` (optional) - Labels for MR as a comma-separated list -- `milestone_id` (optional) - Milestone ID +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `source_branch` | string | yes | The source branch | +| `target_branch` | string | yes | The target branch | +| `title` | string | yes | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `target_project_id` | integer | no | The target project (numeric id) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | ```json { @@ -346,17 +347,19 @@ Updates an existing merge request. You can change the target branch, title, or e PUT /projects/:id/merge_requests/:merge_request_id ``` -Parameters: - -- `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of MR -- `target_branch` - The target branch -- `assignee_id` - Assignee user ID -- `title` - Title of MR -- `description` - Description of MR -- `state_event` - New state (close|reopen|merge) -- `labels` (optional) - Labels for MR as a comma-separated list -- `milestone_id` (optional) - Milestone ID +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a merge request | +| `source_branch` | string | yes | The source branch | +| `target_branch` | string | yes | The target branch | +| `title` | string | yes | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `target_project_id` | integer | no | The target project (numeric id) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | ```json { @@ -807,7 +810,7 @@ Example response: ## Create a todo -Manually creates a todo for the current user on a merge request. +Manually creates a todo for the current user on a merge request. If there already exists a todo for the user on that merge request, status code `304` is returned. diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png Binary files differindex b3b8c20d7bf..92a30da5be0 100644 --- a/doc/project_services/img/mattermost_console_integrations.png +++ b/doc/project_services/img/mattermost_console_integrations.png diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 366e4b2d306..390066c9989 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -197,6 +197,7 @@ incorrectly the JIRA-GitLab integration. Make sure that the user you set up for GitLab to communicate with JIRA has the correct access permission to post comments on a ticket and to also transition the ticket, if you'd like GitLab to also take care of closing them. +JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. ### GitLab is unable to close a ticket diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 1507dfa3abd..6fcbf6f1642 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -22,6 +22,9 @@ commands in Mattermost and then enable the service in GitLab. ### Step 1. Enable custom slash commands in Mattermost +This step is only required when using a source install, omnibus installs will be +preconfigured with the right settings. + The first thing to do in Mattermost is to enable custom slash commands from the administrator console. @@ -32,8 +35,9 @@ the administrator console. --- -1. Click **Custom integrations** and set **Enable Custom Slash Commands** to - true. +1. Click **Custom integrations** and set **Enable Custom Slash Commands**, + **Enable custom integrations to override usernames**, and **Override + custom integrations to override profile picture icons** to true ![Mattermost console](img/mattermost_console_integrations.png) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index d6a0979f6ec..9803937fcf9 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -1,75 +1,141 @@ # SSH -## SSH keys +Git is a distributed version control system, which means you can work locally +but you can also share or "push" your changes to other servers. +Before you can push your changes to a GitLab server +you need a secure communication channel for sharing information. +GitLab uses Public-key or asymmetric cryptography +which encrypts a communication channel by locking it with your "private key" +and allows trusted parties to unlock it with your "public key". +If someone does not have your public key they cannot access the unencrypted message. -An SSH key allows you to establish a secure connection between your -computer and GitLab. Before generating an SSH key in your shell, check if your system -already has one by running the following command: +## Locating an existing SSH key pair + +Before generating a new SSH key check if your system already has one +at the default location by opening a shell, or Command Prompt on Windows, +and running the following command: + +**Windows Command Prompt:** -**Windows Command Line:** ```bash type %userprofile%\.ssh\id_rsa.pub ``` -**GNU/Linux/Mac/PowerShell:** + +**GNU/Linux / macOS / PowerShell:** + ```bash cat ~/.ssh/id_rsa.pub ``` -If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step. +If you see a string starting with `ssh-rsa` you already have an SSH key pair +and you can skip the next step **Generating a new SSH key pair** +and continue onto **Copying your public SSH key to the clipboard**. +If you don't see the string or would like to generate a SSH key pair with a +custom name continue onto the next step. -To generate a new SSH key, use the following command: -```bash -ssh-keygen -t rsa -C "$your_email" -``` -This command will prompt you for a location and filename to store the key -pair and for a password. When prompted for the location and filename, just -press enter to use the default. If you use a different name, the key will not -be used automatically. +## Generating a new SSH key pair -Note: It is a best practice to use a password for an SSH key, but it is not -required and you can skip creating a password by pressing enter. +1. To generate a new SSH key, use the following command: -If you want to change the password of your key later, you can use the following -command: `ssh-keygen -p <keyname>` + **GNU/Linux / macOS:** -Use the command below to show your public key: + ```bash + ssh-keygen -t rsa -C "GitLab" -b 4096 + ``` -**Windows Command Line:** -```bash -type %userprofile%\.ssh\id_rsa.pub -``` -**GNU/Linux/Mac/PowerShell:** -```bash -cat ~/.ssh/id_rsa.pub -``` + **Windows:** -Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your -user profile. Please copy the complete key starting with `ssh-rsa` and ending -with your username and host. + On Windows you will need to download + [PuttyGen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html) + and follow this [documentation article][winputty] to generate a SSH key pair. -To copy your public key to the clipboard, use the code below. Depending on your -OS you'll need to use a different command: +1. Next, you will be prompted to input a file path to save your key pair to. -**Windows Command Line:** -```bash -type %userprofile%\.ssh\id_rsa.pub | clip -``` + If you don't already have an SSH key pair use the suggested path by pressing + enter. Using the suggested path will allow your SSH client + to automatically use the key pair with no additional configuration. -**Windows PowerShell:** -```bash -cat ~/.ssh/id_rsa.pub | clip -``` + If you already have a key pair with the suggested file path, you will need + to input a new file path and declare what host this key pair will be used + for in your `.ssh/config` file, see **Working with non-default SSH key pair paths** + for more information. + +1. Once you have input a file path you will be prompted to input a password to + secure your SSH key pair. It is a best practice to use a password for an SSH + key pair, but it is not required and you can skip creating a password by + pressing enter. + + >**Note:** + If you want to change the password of your key, you can use `ssh-keygen -p <keyname>`. + +1. The next step is to copy the public key as we will need it afterwards. + + To copy your public key to the clipboard, use the appropriate code for your + operating system below: + + **macOS:** + + ```bash + pbcopy < ~/.ssh/id_rsa.pub + ``` + + **GNU/Linux (requires the xclip package):** + + ```bash + xclip -sel clip < ~/.ssh/id_rsa.pub + ``` + + **Windows Command Line:** + + ```bash + type %userprofile%\.ssh\id_rsa.pub | clip + ``` + + **Windows PowerShell:** + + ```bash + cat ~/.ssh/id_rsa.pub | clip + ``` + +1. The final step is to add your public SSH key to GitLab. + + Navigate to the 'SSH Keys' tab in you 'Profile Settings'. + Paste your key in the 'Key' section and give it a relevant 'Title'. + Use an identifiable title like 'Work Laptop - Windows 7' or + 'Home MacBook Pro 15'. + + If you manually copied your public SSH key make sure you copied the entire + key starting with `ssh-rsa` and ending with your email. + +## Working with non-default SSH key pair paths + +If you used a non-default file path for your GitLab SSH key pair, +you must configure your SSH client to find your GitLab SSH private key +for connections to your GitLab server (perhaps gitlab.com). + +For OpenSSH clients this is configured in the `~/.ssh/config` file. +Below are two example host configurations using their own key: -**Mac:** -```bash -pbcopy < ~/.ssh/id_rsa.pub ``` +# GitLab.com server +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/.ssh/config/private-key-filename-01 -**GNU/Linux (requires xclip):** -```bash -xclip -sel clip < ~/.ssh/id_rsa.pub +# Private GitLab server +Host gitlab.company.com +RSAAuthentication yes +IdentityFile ~/.ssh/config/private-key-filename ``` +Due to the wide variety of SSH clients and their very large number of +configuration options, further explanation of these topics is beyond the scope +of this document. + +Public SSH keys need to be unique, as they will bind to your account. +Your SSH key is the only identifier you'll have when pushing code via SSH. +That's why it needs to uniquely map to a single user. + ## Deploy keys Deploy keys allow read-only access to multiple projects with a single SSH @@ -89,10 +155,10 @@ If you want to add the same key to another project, please enable it in the list that says 'Deploy keys from projects available to you'. All the deploy keys of all the projects you have access to are available. This project access can happen through being a direct member of the project, or through -a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more -information. +a group. -Deploy keys can be shared between projects, you just need to add them to each project. +Deploy keys can be shared between projects, you just need to add them to each +project. ## Applications @@ -100,33 +166,4 @@ Deploy keys can be shared between projects, you just need to add them to each pr How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration -## Tip: Non-default OpenSSH key file names or locations - -If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: - -``` -# -# Main gitlab.com server -# -Host gitlab.com -RSAAuthentication yes -IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename -User mygitlabusername -``` - -Another example -``` -# -# Our company's internal GitLab server -# -Host my-gitlab.company.com -RSAAuthentication yes -IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename -``` - -Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. - -Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. - -Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll -have when pushing code via SSH. That's why it needs to uniquely map to a single user.
\ No newline at end of file +[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index 576b943b98c..3f58493fa63 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -113,7 +113,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: ```sh -git diff origin/8-13-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example +git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example ``` #### Git configuration @@ -131,10 +131,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes: ```sh # For HTTPS configurations -git diff origin/8-13-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl +git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl # For HTTP configurations -git diff origin/8-13-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab +git diff origin/8-14-stable:lib/support/nginx/gitlab origin/8-15-stable:lib/support/nginx/gitlab ``` If you are using Apache instead of NGINX please see the updated [Apache templates]. diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8db2678b368..8b0f8deadfa 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -111,7 +111,7 @@ module API if id =~ /^\d+$/ Group.find_by(id: id) else - Group.find_by(path: id) + Group.find_by_full_path(id) end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 253460830ff..55bdbc6a47c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -30,6 +30,7 @@ module API optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' end end @@ -78,7 +79,8 @@ module API post ":id/merge_requests" do authorize! :create_merge_request, user_project - mr_params = declared_params + mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute @@ -147,13 +149,15 @@ module API desc: 'Status of the merge request' use :optional_params at_least_one_of :title, :target_branch, :description, :assignee_id, - :milestone_id, :labels, :state_event + :milestone_id, :labels, :state_event, + :remove_source_branch end put path do merge_request = user_project.merge_requests.find(params.delete(:merge_request_id)) authorize! :update_merge_request, merge_request mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index 5711d96a586..bae4db1ca4d 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -4,7 +4,7 @@ class GroupUrlConstrainer return false unless valid?(id) - Group.find_by(path: id).present? + Group.find_by_full_path(id).present? end private diff --git a/lib/event_filter.rb b/lib/event_filter.rb index 21f6a9a762b..515095af1c2 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -14,6 +14,10 @@ class EventFilter 'merged' end + def issue + 'issue' + end + def comments 'comments' end @@ -32,32 +36,20 @@ class EventFilter end def apply_filter(events) - return events unless params.present? - - filter = params.dup - actions = [] + return events if params.blank? || params == EventFilter.all - case filter + case params when EventFilter.push - actions = [Event::PUSHED] + events.where(action: Event::PUSHED) when EventFilter.merged - actions = [Event::MERGED] + events.where(action: Event::MERGED) when EventFilter.comments - actions = [Event::COMMENTED] + events.where(action: Event::COMMENTED) when EventFilter.team - actions = [Event::JOINED, Event::LEFT, Event::EXPIRED] - when EventFilter.all - actions = [ - Event::PUSHED, - Event::MERGED, - Event::COMMENTED, - Event::JOINED, - Event::LEFT, - Event::EXPIRED - ] + events.where(action: [Event::JOINED, Event::LEFT, Event::EXPIRED]) + when EventFilter.issue + events.where(action: [Event::CREATED, Event::UPDATED, Event::CLOSED, Event::REOPENED]) end - - events.where(action: actions) end def options(key) @@ -73,6 +65,10 @@ class EventFilter end def active?(key) - params.include? key + if params.present? + params.include? key + else + key == EventFilter.all + end end end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 1a22ad9acf5..9667df4ffb8 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -6,7 +6,7 @@ module Gitlab module Asciidoc DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', - 'env-gitlab', 'source-highlighter=html-pipeline' + 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font' ].freeze # Public: Converts the provided Asciidoc markup into HTML. diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb new file mode 100644 index 00000000000..b2f896f2211 --- /dev/null +++ b/lib/gitlab/ci/status/factory.rb @@ -0,0 +1,43 @@ +module Gitlab + module Ci + module Status + class Factory + attr_reader :subject + + def initialize(subject) + @subject = subject + end + + def fabricate! + if extended_status + extended_status.new(core_status) + else + core_status + end + end + + private + + def subject_status + @subject_status ||= subject.status + end + + def core_status + Gitlab::Ci::Status + .const_get(subject_status.capitalize) + .new(subject) + end + + def extended_status + @extended ||= extended_statuses.find do |status| + status.matches?(subject) + end + end + + def extended_statuses + [] + end + end + end + end +end diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb index 71d27bf7cf5..4ac4ec671d0 100644 --- a/lib/gitlab/ci/status/pipeline/factory.rb +++ b/lib/gitlab/ci/status/pipeline/factory.rb @@ -2,35 +2,15 @@ module Gitlab module Ci module Status module Pipeline - class Factory - EXTENDED_STATUSES = [Pipeline::SuccessWithWarnings] - - def initialize(pipeline) - @pipeline = pipeline - @status = pipeline.status || :created - end - - def fabricate! - if extended_status - extended_status.new(core_status) - else - core_status - end - end - + class Factory < Status::Factory private - def core_status - Gitlab::Ci::Status - .const_get(@status.capitalize) - .new(@pipeline) - .extend(Status::Pipeline::Common) + def extended_statuses + [Pipeline::SuccessWithWarnings] end - def extended_status - @extended ||= EXTENDED_STATUSES.find do |status| - status.matches?(@pipeline) - end + def core_status + super.extend(Status::Pipeline::Common) end end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb new file mode 100644 index 00000000000..14c437d2b98 --- /dev/null +++ b/lib/gitlab/ci/status/stage/common.rb @@ -0,0 +1,24 @@ +module Gitlab + module Ci + module Status + module Stage + module Common + def has_details? + true + end + + def details_path + namespace_project_pipeline_path(@subject.project.namespace, + @subject.project, + @subject.pipeline, + anchor: @subject.name) + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb new file mode 100644 index 00000000000..c6522d5ada1 --- /dev/null +++ b/lib/gitlab/ci/status/stage/factory.rb @@ -0,0 +1,15 @@ +module Gitlab + module Ci + module Status + module Stage + class Factory < Status::Factory + private + + def core_status + super.extend(Status::Stage::Common) + end + end + end + end + end +end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 06a783ebc1c..e50e54b6e99 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -22,7 +22,7 @@ module Gitlab sha: pipeline.sha, before_sha: pipeline.before_sha, status: pipeline.status, - stages: pipeline.stages, + stages: pipeline.stages_name, created_at: pipeline.created_at, finished_at: pipeline.finished_at, duration: pipeline.duration diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 47d8599e298..35212992698 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -68,7 +68,7 @@ module Gitlab end def merge_requests - merge_requests = MergeRequest.in_projects(project_ids_relation) + merge_requests = MergeRequestsFinder.new(current_user).execute.in_projects(project_ids_relation) if query =~ /[#!](\d+)\z/ merge_requests = merge_requests.where(iid: $1) else diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb index 193a3f6b5a3..415c264e0dd 100644 --- a/spec/controllers/projects/todo_controller_spec.rb +++ b/spec/controllers/projects/todo_controller_spec.rb @@ -110,7 +110,7 @@ describe Projects::TodosController do end end - context 'when not authorized' do + context 'when not authorized for project' do it 'does not create todo for merge request user has no access to' do sign_in(user) expect do @@ -128,6 +128,19 @@ describe Projects::TodosController do expect(response).to have_http_status(302) end end + + context 'when not authorized for merge_request' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + sign_in(user) + end + + it "doesn't create todo" do + expect{ go }.not_to change { user.todos.count } + expect(response).to have_http_status(404) + end + end end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 48d69377461..b56c7880b64 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -22,7 +22,6 @@ describe SessionsController do it 'authenticates user correctly' do post(:create, user: { login: user.username, password: user.password }) - expect(response).to set_flash.to /Signed in successfully/ expect(subject.current_user). to eq user end diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb new file mode 100644 index 00000000000..ee3b17b8bf1 --- /dev/null +++ b/spec/factories/ci/stages.rb @@ -0,0 +1,13 @@ +FactoryGirl.define do + factory :ci_stage, class: Ci::Stage do + transient do + name 'test' + status nil + pipeline factory: :ci_empty_pipeline + end + + initialize_with do + Ci::Stage.new(pipeline, name: name, status: status) + end + end +end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index c7fe622c477..e1b97b31e5d 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -85,14 +85,14 @@ feature 'Environments page', :feature, :js do end scenario 'does show a play button' do - find('.dropdown-play-icon-container').click + find('.js-dropdown-play-icon-container').click expect(page).to have_content(manual.name.humanize) end scenario 'does allow to play manual action', js: true do expect(manual).to be_skipped - find('.dropdown-play-icon-container').click + find('.js-dropdown-play-icon-container').click expect(page).to have_content(manual.name.humanize) expect { click_link(manual.name.humanize) } diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb new file mode 100644 index 00000000000..b6134540273 --- /dev/null +++ b/spec/features/merge_requests/target_branch_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'Target branch', feature: true do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + + def path_to_merge_request + namespace_project_merge_request_path( + project.namespace, + project, merge_request + ) + end + + before do + login_as user + project.team << [user, :master] + end + + it 'shows link to target branch' do + visit path_to_merge_request + expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch)) + end + + context 'when branch was deleted' do + before do + DeleteBranchService.new(project, user).execute('feature') + visit path_to_merge_request + end + + it 'shows a message about missing target branch' do + expect(page).to have_content( + 'Target branch feature does not exist' + ) + end + + it 'does not show link to target branch' do + expect(page).not_to have_link('feature') + end + end +end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 09aa6758b5c..3bb33394be7 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -182,6 +182,44 @@ describe 'Edit Project Settings', feature: true do expect(page).not_to have_content("Comments") end end + + # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272 + it "hides comments activity tab only on disabled issues, merge requests and repository" do + select "Disabled", from: "project_project_feature_attributes_issues_access_level" + + save_changes_and_check_activity_tab do + expect(page).to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + + select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level" + + save_changes_and_check_activity_tab do + expect(page).to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + + select "Disabled", from: "project_project_feature_attributes_repository_access_level" + + save_changes_and_check_activity_tab do + expect(page).not_to have_content("Comments") + end + + visit edit_namespace_project_path(project.namespace, project) + end + + def save_changes_and_check_activity_tab + click_button "Save changes" + wait_for_ajax + + visit activity_namespace_project_path(project.namespace, project) + + page.within(".event-filter") do + yield + end + end end # Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/24056 diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb new file mode 100644 index 00000000000..7d0065ee2c4 --- /dev/null +++ b/spec/features/projects/members/group_members_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +feature 'Projects members', feature: true do + let(:user) { create(:user) } + let(:developer) { create(:user) } + let(:group) { create(:group, :public, :access_requestable) } + let(:project) { create(:empty_project, :public, :access_requestable, creator: user, group: group) } + let(:project_invitee) { create(:project_member, project: project, invite_token: '123', invite_email: 'test1@abc.com', user: nil) } + let(:group_invitee) { create(:group_member, group: group, invite_token: '123', invite_email: 'test2@abc.com', user: nil) } + let(:project_requester) { create(:user) } + let(:group_requester) { create(:user) } + + background do + project.team << [developer, :developer] + group.add_owner(user) + login_as(user) + end + + context 'with a group invitee' do + before do + group_invitee + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'does not appear in the project members page' do + page.within first('.content-list') do + expect(page).not_to have_content('test2@abc.com') + end + end + end + + context 'with a group and a project invitee' do + before do + group_invitee + project_invitee + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'shows the project invitee, the project developer, and the group owner' do + page.within first('.content-list') do + expect(page).to have_content('test1@abc.com') + expect(page).not_to have_content('test2@abc.com') + + # Project developer + expect(page).to have_content(developer.name) + + # Group owner + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end + end + end + + context 'with a group requester' do + before do + group.request_access(group_requester) + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'does not appear in the project members page' do + page.within first('.content-list') do + expect(page).not_to have_content(group_requester.name) + end + end + end + + context 'with a group and a project requesters' do + before do + group.request_access(group_requester) + project.request_access(project_requester) + visit namespace_project_project_members_path(project.namespace, project) + end + + scenario 'shows the project requester, the project developer, and the group owner' do + page.within first('.content-list') do + expect(page).to have_content(project_requester.name) + expect(page).not_to have_content(group_requester.name) + end + + page.within all('.content-list').last do + # Project developer + expect(page).to have_content(developer.name) + + # Group owner + expect(page).to have_content(user.name) + expect(page).to have_content(group.name) + end + end + end +end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb new file mode 100644 index 00000000000..4bfaa499272 --- /dev/null +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +feature 'Project settings > Merge Requests', feature: true, js: true do + include GitlabRoutingHelper + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as(user) + end + + context 'when Merge Request and Builds are initially enabled' do + before do + project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) + end + + context 'when Builds are initially enabled' do + before do + project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) + visit edit_project_path(project) + end + + scenario 'shows the Merge Requests settings' do + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" + + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end + + context 'when Builds are initially disabled' do + before do + project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) + visit edit_project_path(project) + end + + scenario 'shows the Merge Requests settings that do not depend on Builds feature' do + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" + + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end + end + + context 'when Merge Request are initially disabled' do + before do + project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED) + visit edit_project_path(project) + end + + scenario 'does not show the Merge Requests settings' do + expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') + + select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" + + expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') + end + end +end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index b750f27ea72..be21b403084 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -163,8 +163,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: click_on "Sign in via U2F device" expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end @@ -178,7 +177,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end @@ -234,7 +233,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') end end end @@ -275,7 +274,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: expect(page.body).to match('We heard back from your U2F device') click_on "Authenticate via U2F Device" - expect(page.body).to match('Signed in successfully') + expect(page.body).to match('href="/users/sign_out"') logout end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 837e7afa7e8..468bcc7badc 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -60,15 +60,58 @@ describe DiffHelper do end describe '#diff_line_content' do - it 'returns non breaking space when line is empty' do - expect(diff_line_content(nil)).to eq(' ') - end - - it 'returns the line itself' do - expect(diff_line_content(diff_file.diff_lines.first.text)). - to eq('@@ -6,12 +6,18 @@ module Popen') - expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') - expect(diff_file.diff_lines.first.new_pos).to eq(6) + context 'when the line is empty' do + it 'returns a non breaking space' do + expect(diff_line_content(nil)).to eq(' ') + end + + it 'returns an HTML-safe string' do + expect(diff_line_content(nil)).to be_html_safe + end + end + + context 'when the line is not empty' do + context 'when the line starts with +, -, or a space' do + it 'strips the first character' do + expect(diff_line_content('+new line')).to eq('new line') + expect(diff_line_content('-new line')).to eq('new line') + expect(diff_line_content(' new line')).to eq('new line') + end + + context 'when the line is HTML-safe' do + it 'returns an HTML-safe string' do + expect(diff_line_content('+new line'.html_safe)).to be_html_safe + expect(diff_line_content('-new line'.html_safe)).to be_html_safe + expect(diff_line_content(' new line'.html_safe)).to be_html_safe + end + end + + context 'when the line is not HTML-safe' do + it 'returns a non-HTML-safe string' do + expect(diff_line_content('+new line')).not_to be_html_safe + expect(diff_line_content('-new line')).not_to be_html_safe + expect(diff_line_content(' new line')).not_to be_html_safe + end + end + end + + context 'when the line does not start with a +, -, or a space' do + it 'returns the string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).to eq('@@ -6,12 +6,18 @@ module Popen') + end + + context 'when the line is HTML-safe' do + it 'returns an HTML-safe string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen'.html_safe)).to be_html_safe + end + end + + context 'when the line is not HTML-safe' do + it 'returns a non-HTML-safe string' do + expect(diff_line_content('@@ -6,12 +6,18 @@ module Popen')).not_to be_html_safe + end + end + end end end diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 76e81233e89..4bae3f30bb5 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -8,7 +8,7 @@ describe('Actions Component', () => { fixture.load('environments/element.html'); }); - it('Should render a dropdown with the provided actions', () => { + it('should render a dropdown with the provided actions', () => { const actionsMock = [ { name: 'bar', @@ -24,6 +24,7 @@ describe('Actions Component', () => { el: document.querySelector('.test-dom-element'), propsData: { actions: actionsMock, + playIconSvg: '<svg></svg>', }, }); @@ -34,4 +35,33 @@ describe('Actions Component', () => { component.$el.querySelector('.dropdown-menu li a').getAttribute('href'), ).toEqual(actionsMock[0].play_path); }); + + it('should render a dropdown with the provided svg', () => { + const actionsMock = [ + { + name: 'bar', + play_path: 'https://gitlab.com/play', + }, + { + name: 'foo', + play_path: '#', + }, + ]; + + const component = new window.gl.environmentsList.ActionsComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + actions: actionsMock, + playIconSvg: '<svg></svg>', + }, + }); + + expect( + component.$el.querySelector('.js-dropdown-play-icon-container').children, + ).toContain('svg'); + + expect( + component.$el.querySelector('.js-action-play-icon-container').children, + ).toContain('svg'); + }); }); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 156506ef28f..9f82567c35b 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -7,12 +7,12 @@ describe('External URL Component', () => { fixture.load('environments/element.html'); }); - it('should link to the provided external_url', () => { + it('should link to the provided externalUrl prop', () => { const externalURL = 'https://gitlab.com'; const component = new window.gl.environmentsList.ExternalUrlComponent({ el: document.querySelector('.test-dom-element'), propsData: { - external_url: externalURL, + externalUrl: externalURL, }, }); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 29449bbbd9e..77ba0ab38ec 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -9,24 +9,24 @@ describe('Rollback Component', () => { fixture.load('environments/element.html'); }); - it('Should link to the provided retry_url', () => { + it('Should link to the provided retryUrl', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: true, + retryUrl: retryURL, + isLastDeployment: true, }, }); expect(component.$el.getAttribute('href')).toEqual(retryURL); }); - it('Should render Re-deploy label when is_last_deployment is true', () => { + it('Should render Re-deploy label when isLastDeployment is true', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: true, + retryUrl: retryURL, + isLastDeployment: true, }, }); @@ -34,12 +34,12 @@ describe('Rollback Component', () => { }); - it('Should render Rollback label when is_last_deployment is false', () => { + it('Should render Rollback label when isLastDeployment is false', () => { const component = new window.gl.environmentsList.RollbackComponent({ el: document.querySelector('.test-dom-element'), propsData: { - retry_url: retryURL, - is_last_deployment: false, + retryUrl: retryURL, + isLastDeployment: false, }, }); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index b842be4da61..84a41b2bf46 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -13,7 +13,7 @@ describe('Stop Component', () => { component = new window.gl.environmentsList.StopComponent({ el: document.querySelector('.test-dom-element'), propsData: { - stop_url: stopURL, + stopUrl: stopURL, }, }); }); diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml index 95e248cadf8..5477c6075f0 100644 --- a/spec/javascripts/fixtures/event_filter.html.haml +++ b/spec/javascripts/fixtures/event_filter.html.haml @@ -12,6 +12,10 @@ %span Merge events %li + %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"} + %span + Issue events + %li %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"} %span Comments diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml new file mode 100644 index 00000000000..12b8d423cbe --- /dev/null +++ b/spec/javascripts/fixtures/signin_tabs.html.haml @@ -0,0 +1,5 @@ +%ul.nav-tabs + %li + %a.active{ id: 'standard', href: '#standard'} Standard + %li + %a{ id: 'ldap', href: '#ldap'} Ldap diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 new file mode 100644 index 00000000000..9a9fb22255b --- /dev/null +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -0,0 +1,53 @@ +/*= require signin_tabs_memoizer */ + +((global) => { + describe('SigninTabsMemoizer', () => { + const fixtureTemplate = 'signin_tabs.html'; + const tabSelector = 'ul.nav-tabs'; + const currentTabKey = 'current_signin_tab'; + let memo; + + function createMemoizer() { + memo = new global.ActiveTabMemoizer({ + currentTabKey, + tabSelector, + }); + return memo; + } + + fixture.preload(fixtureTemplate); + + beforeEach(() => { + fixture.load(fixtureTemplate); + }); + + it('does nothing if no tab was previously selected', () => { + createMemoizer(); + + expect(document.querySelector('li a.active').getAttribute('id')).toEqual('standard'); + }); + + it('shows last selected tab on boot', () => { + createMemoizer().saveData('#ldap'); + const fakeTab = { + click: () => {}, + }; + spyOn(document, 'querySelector').and.returnValue(fakeTab); + spyOn(fakeTab, 'click'); + + memo.bootstrap(); + + // verify that triggers click on the last selected tab + expect(document.querySelector).toHaveBeenCalledWith(`${tabSelector} a[href="#ldap"]`); + expect(fakeTab.click).toHaveBeenCalled(); + }); + + it('saves last selected tab on change', () => { + createMemoizer(); + + document.getElementById('standard').click(); + + expect(memo.readData()).toEqual('#standard'); + }); + }); +})(window); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d170517dd9b..26dfdb94aae 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -10,12 +10,12 @@ describe('Commit component', () => { el: document.querySelector('.test-commit-container'), propsData: { tag: false, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: 'Commit message', author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', @@ -34,18 +34,19 @@ describe('Commit component', () => { props = { tag: true, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: 'Commit message', author: { avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', username: 'jschatz1', }, + commitIconSvg: '<svg></svg>', }; component = new window.gl.CommitComponent({ @@ -59,20 +60,24 @@ describe('Commit component', () => { }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commit_ref.ref_url); + expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commit_ref.name); + expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); }); it('should render the commit short sha with a link to the commit url', () => { - expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commit_url); - expect(component.$el.querySelector('.commit-id').textContent).toContain(props.short_sha); + expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl); + expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha); + }); + + it('should render the given commitIconSvg', () => { + expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); }); describe('Given commit title and author props', () => { - it('Should render a link to the author profile', () => { + it('should render a link to the author profile', () => { expect( component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), ).toEqual(props.author.web_url); @@ -91,7 +96,7 @@ describe('Commit component', () => { it('should render the commit title', () => { expect( component.$el.querySelector('a.commit-row-message').getAttribute('href'), - ).toEqual(props.commit_url); + ).toEqual(props.commitUrl); expect( component.$el.querySelector('a.commit-row-message').textContent, ).toContain(props.title); @@ -99,16 +104,16 @@ describe('Commit component', () => { }); describe('When commit title is not provided', () => { - it('Should render default message', () => { + it('should render default message', () => { fixture.set('<div class="test-commit-container"></div>'); props = { tag: false, - commit_ref: { + commitRef: { name: 'master', ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', }, - commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - short_sha: 'b7836edd', + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', title: null, author: {}, }; diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index 892554f2870..96dacdc5cd2 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -10,6 +10,13 @@ describe GroupUrlConstrainer, lib: true do it { expect(subject.matches?(request)).to be_truthy } end + context 'valid request for nested group' do + let!(:nested_group) { create(:group, path: 'nested', parent: group) } + let!(:request) { build_request('gitlab/nested') } + + it { expect(subject.matches?(request)).to be_truthy } + end + context 'invalid request' do let(:request) { build_request('foo') } diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index a6d8e6927e0..ec2f66b1136 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -7,6 +7,10 @@ describe EventFilter, lib: true do let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } + let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } + let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } + let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } + let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } @@ -21,6 +25,11 @@ describe EventFilter, lib: true do expect(events).to contain_exactly(merged_event) end + it 'applies issue filter' do + events = EventFilter.new(EventFilter.issue).apply_filter(Event.all) + expect(events).to contain_exactly(created_event, updated_event, closed_event, reopened_event) + end + it 'applies comments filter' do events = EventFilter.new(EventFilter.comments).apply_filter(Event.all) expect(events).to contain_exactly(comments_event) @@ -33,17 +42,17 @@ describe EventFilter, lib: true do it 'applies all filter' do events = EventFilter.new(EventFilter.all).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies no filter' do events = EventFilter.new(nil).apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end it 'applies unknown filter' do events = EventFilter.new('').apply_filter(Event.all) - expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event) + expect(events).to contain_exactly(push_event, merged_event, created_event, updated_event, closed_event, reopened_event, comments_event, joined_event, left_event) end end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb new file mode 100644 index 00000000000..d5bd7f7102b --- /dev/null +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Factory do + subject do + described_class.new(object) + end + + let(:status) { subject.fabricate! } + + context 'when object has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + let(:object) { double(status: core_status) } + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/stage/common_spec.rb b/spec/lib/gitlab/ci/status/stage/common_spec.rb new file mode 100644 index 00000000000..f3259c6f23e --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/common_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Common do + let(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + subject do + Class.new(Gitlab::Ci::Status::Core) + .new(stage).extend(described_class) + end + + it 'does not have action' do + expect(subject).not_to have_action + end + + it 'has details' do + expect(subject).to have_details + end + + it 'links to the pipeline details page' do + expect(subject.details_path) + .to include "pipelines/#{pipeline.id}" + expect(subject.details_path) + .to include "##{stage.name}" + end +end diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb new file mode 100644 index 00000000000..17929665c83 --- /dev/null +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Stage::Factory do + let(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') } + + subject do + described_class.new(stage) + end + + let(:status) do + subject.fabricate! + end + + context 'when stage has a core status' do + HasStatus::AVAILABLE_STATUSES.each do |core_status| + context "when core status is #{core_status}" do + before do + create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status) + create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status) + create(:ci_build, pipeline: pipeline, stage: 'build', status: :failed) + end + + it "fabricates a core status #{core_status}" do + expect(status).to be_a( + Gitlab::Ci::Status.const_get(core_status.capitalize)) + end + + it 'extends core status with common stage methods' do + expect(status).to have_details + expect(status.details_path).to include "pipelines/#{pipeline.id}" + expect(status.details_path).to include "##{stage.name}" + end + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7e00e214c6e..8e1a28f2723 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -188,6 +188,7 @@ project: - project_feature - authorized_users - project_authorizations +- route award_emoji: - awardable - user diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index f23e3522625..9614aad3e73 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -40,6 +40,15 @@ describe Gitlab::SearchResults do expect(results.milestones_count).to eq(1) end end + + it 'includes merge requests from source and target projects' do + forked_project = create(:empty_project, forked_from_project: project) + merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') + + results = described_class.new(user, Project.where(id: forked_project.id), 'foo') + + expect(results.objects('merge_requests')).to include merge_request_2 + end end it 'does not list issues on private projects' do @@ -152,4 +161,11 @@ describe Gitlab::SearchResults do expect(results.issues_count).to eq 5 end end + + it 'does not list merge requests on projects with limited access' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + expect(results.objects('merge_requests')).not_to include merge_request + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3f93d9ddf19..8158e71dd55 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -20,8 +20,6 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } - it { is_expected.to delegate_method(:stages).to(:statuses) } - describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do @@ -125,16 +123,55 @@ describe Ci::Pipeline, models: true do end describe '#stages' do - let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project } - subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages } - before do - FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1 - FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0 + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success') + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed') + create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running') + create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success') + end + + subject { pipeline.stages } + + context 'stages list' do + it 'returns ordered list of stages' do + expect(subject.map(&:name)).to eq(%w[build test deploy]) + end + end + + it 'returns a valid number of stages' do + expect(pipeline.stages_count).to eq(3) + end + + it 'returns a valid names of stages' do + expect(pipeline.stages_name).to eq(['build', 'test', 'deploy']) end - it 'return all stages' do - is_expected.to eq(%w(build test)) + context 'stages with statuses' do + let(:statuses) do + subject.map do |stage| + [stage.name, stage.status] + end + end + + it 'returns list of stages with statuses' do + expect(statuses).to eq([['build', 'failed'], + ['test', 'success'], + ['deploy', 'running'] + ]) + end + + context 'when build is retried' do + before do + create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success') + end + + it 'ignores the previous state' do + expect(statuses).to eq([['build', 'success'], + ['test', 'success'], + ['deploy', 'running'] + ]) + end + end end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb new file mode 100644 index 00000000000..f232761dba2 --- /dev/null +++ b/spec/models/ci/stage_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +describe Ci::Stage, models: true do + let(:stage) { build(:ci_stage) } + let(:pipeline) { stage.pipeline } + let(:stage_name) { stage.name } + + describe '#expectations' do + subject { stage } + + it { is_expected.to include_module(StaticModel) } + + it { is_expected.to respond_to(:pipeline) } + it { is_expected.to respond_to(:name) } + + it { is_expected.to delegate_method(:project).to(:pipeline) } + end + + describe '#statuses' do + let!(:stage_build) { create_job(:ci_build) } + let!(:commit_status) { create_job(:commit_status) } + let!(:other_build) { create_job(:ci_build, stage: 'other stage') } + + subject { stage.statuses } + + it "returns only matching statuses" do + is_expected.to contain_exactly(stage_build, commit_status) + end + end + + describe '#builds' do + let!(:stage_build) { create_job(:ci_build) } + let!(:commit_status) { create_job(:commit_status) } + + subject { stage.builds } + + it "returns only builds" do + is_expected.to contain_exactly(stage_build) + end + end + + describe '#status' do + subject { stage.status } + + context 'if status is already defined' do + let(:stage) { build(:ci_stage, status: 'success') } + + it "returns defined status" do + is_expected.to eq('success') + end + end + + context 'if status has to be calculated' do + let!(:stage_build) { create_job(:ci_build, status: :failed) } + + it "returns status of a build" do + is_expected.to eq('failed') + end + + context 'and builds are retried' do + let!(:new_build) { create_job(:ci_build, status: :success) } + + it "returns status of latest build" do + is_expected.to eq('success') + end + end + end + end + + describe '#detailed_status' do + subject { stage.detailed_status } + + context 'when build is created' do + let!(:stage_build) { create_job(:ci_build, status: :created) } + + it 'returns detailed status for created stage' do + expect(subject.text).to eq 'created' + end + end + + context 'when build is pending' do + let!(:stage_build) { create_job(:ci_build, status: :pending) } + + it 'returns detailed status for pending stage' do + expect(subject.text).to eq 'pending' + end + end + + context 'when build is running' do + let!(:stage_build) { create_job(:ci_build, status: :running) } + + it 'returns detailed status for running stage' do + expect(subject.text).to eq 'running' + end + end + + context 'when build is successful' do + let!(:stage_build) { create_job(:ci_build, status: :success) } + + it 'returns detailed status for successful stage' do + expect(subject.text).to eq 'passed' + end + end + + context 'when build is failed' do + let!(:stage_build) { create_job(:ci_build, status: :failed) } + + it 'returns detailed status for failed stage' do + expect(subject.text).to eq 'failed' + end + end + + context 'when build is canceled' do + let!(:stage_build) { create_job(:ci_build, status: :canceled) } + + it 'returns detailed status for canceled stage' do + expect(subject.text).to eq 'canceled' + end + end + + context 'when build is skipped' do + let!(:stage_build) { create_job(:ci_build, status: :skipped) } + + it 'returns detailed status for skipped stage' do + expect(subject.text).to eq 'skipped' + end + end + end + + def create_job(type, status: 'success', stage: stage_name) + create(type, pipeline: pipeline, stage: stage, status: status) + end +end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index d89d4342dea..30782ca75a0 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -137,26 +137,25 @@ describe CommitRange, models: true do end describe '#has_been_reverted?' do - it 'returns true if the commit has been reverted' do - issue = create(:issue) + let(:issue) { create(:issue) } + let(:user) { issue.author } + it 'returns true if the commit has been reverted' do create(:note_on_issue, noteable: issue, system: true, - note: commit1.revert_description, + note: commit1.revert_description(user), project: issue.project) expect_any_instance_of(Commit).to receive(:reverts_commit?). - with(commit1). + with(commit1, user). and_return(true) - expect(commit1.has_been_reverted?(nil, issue)).to eq(true) + expect(commit1.has_been_reverted?(user, issue)).to eq(true) end it 'returns false a commit has not been reverted' do - issue = create(:issue) - - expect(commit1.has_been_reverted?(nil, issue)).to eq(false) + expect(commit1.has_been_reverted?(user, issue)).to eq(false) end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index eb482c7f913..0935fc0561c 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -179,25 +179,26 @@ eos describe '#reverts_commit?' do let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") } + let(:user) { commit.author } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } context 'commit has no description' do before { allow(commit).to receive(:description?).and_return(false) } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description does not revert commit" do before { allow(commit).to receive(:description).and_return("Foo Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description reverts commit" do before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end context "another_commit's description reverts merged merge request" do @@ -207,7 +208,7 @@ eos allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") end - it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 80c2a1bc7a9..1ec08c2a9d0 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -175,7 +175,7 @@ describe CommitStatus, models: true do end it 'returns statuses without what we want to ignore' do - is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9)) + is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9)) end end @@ -200,49 +200,6 @@ describe CommitStatus, models: true do end end - describe '#stages' do - before do - create :commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success' - create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed' - create :commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running' - create :commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success' - end - - context 'stages list' do - subject { CommitStatus.where(pipeline: pipeline).stages } - - it 'returns ordered list of stages' do - is_expected.to eq(%w[build test deploy]) - end - end - - context 'stages with statuses' do - subject { CommitStatus.where(pipeline: pipeline).latest.stages_status } - - it 'returns list of stages with statuses' do - is_expected.to eq({ - 'build' => 'failed', - 'test' => 'success', - 'deploy' => 'running' - }) - end - - context 'when build is retried' do - before do - create :commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success' - end - - it 'ignores a previous state' do - is_expected.to eq({ - 'build' => 'success', - 'test' => 'success', - 'deploy' => 'running' - }) - end - end - end - end - describe '#commit' do it 'returns commit pipeline has been created for' do expect(commit_status.commit).to eq project.commit diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 9defb17dc92..4d0f51fe82a 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -48,7 +48,7 @@ describe HasStatus do [create(type, status: :failed, allow_failure: true)] end - it { is_expected.to eq 'success' } + it { is_expected.to eq 'skipped' } end context 'success and canceled' do diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb new file mode 100644 index 00000000000..0acefc0c1d5 --- /dev/null +++ b/spec/models/concerns/routable_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Group, 'Routable' do + let!(:group) { create(:group) } + + describe 'Associations' do + it { is_expected.to have_one(:route).dependent(:destroy) } + end + + describe 'Callbacks' do + it 'creates route record on create' do + expect(group.route.path).to eq(group.path) + end + + it 'updates route record on path change' do + group.update_attributes(path: 'wow') + + expect(group.route.path).to eq('wow') + end + + it 'ensure route path uniqueness across different objects' do + create(:group, parent: group, path: 'xyz') + duplicate = build(:project, namespace: group, path: 'xyz') + + expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Route path has already been taken, Route is invalid') + end + end + + describe '.find_by_full_path' do + let!(:nested_group) { create(:group, parent: group) } + + it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) } + it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) } + it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) } + it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } + end + + describe '.where_paths_in' do + context 'without any paths' do + it 'returns an empty relation' do + expect(described_class.where_paths_in([])).to eq([]) + end + end + + context 'without any valid paths' do + it 'returns an empty relation' do + expect(described_class.where_paths_in(%w[unknown])).to eq([]) + end + end + + context 'with valid paths' do + let!(:nested_group) { create(:group, parent: group) } + + it 'returns the projects matching the paths' do + result = described_class.where_paths_in([group.to_param, nested_group.to_param]) + + expect(result).to contain_exactly(group, nested_group) + end + + it 'returns projects regardless of the casing of paths' do + result = described_class.where_paths_in([group.to_param.upcase, nested_group.to_param.upcase]) + + expect(result).to contain_exactly(group, nested_group) + end + end + end +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index ba0ed4a3603..7f82e85563b 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -124,4 +124,12 @@ describe Namespace, models: true do expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name") end end + + describe '#full_path' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_path).to eq(group.path) } + it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 4d57330ed1c..21ff238841e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -478,35 +478,6 @@ describe Project, models: true do end end - describe '.find_with_namespace' do - context 'with namespace' do - before do - @group = create :group, name: 'gitlab' - @project = create(:project, name: 'gitlabhq', namespace: @group) - end - - it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) } - it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) } - it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } - end - - context 'when multiple projects using a similar name exist' do - let(:group) { create(:group, name: 'gitlab') } - - let!(:project1) do - create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group) - end - - let!(:project2) do - create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group) - end - - it 'returns the row where the path matches literally' do - expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2) - end - end - end - describe '#to_param' do context 'with namespace' do before do @@ -1548,39 +1519,6 @@ describe Project, models: true do end end - describe '.where_paths_in' do - context 'without any paths' do - it 'returns an empty relation' do - expect(Project.where_paths_in([])).to eq([]) - end - end - - context 'without any valid paths' do - it 'returns an empty relation' do - expect(Project.where_paths_in(%w[foo])).to eq([]) - end - end - - context 'with valid paths' do - let!(:project1) { create(:project) } - let!(:project2) { create(:project) } - - it 'returns the projects matching the paths' do - projects = Project.where_paths_in([project1.path_with_namespace, - project2.path_with_namespace]) - - expect(projects).to contain_exactly(project1, project2) - end - - it 'returns projects regardless of the casing of paths' do - projects = Project.where_paths_in([project1.path_with_namespace.upcase, - project2.path_with_namespace.upcase]) - - expect(projects).to contain_exactly(project1, project2) - end - end - end - describe 'change_head' do let(:project) { create(:project) } diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb new file mode 100644 index 00000000000..6f491fdf9a0 --- /dev/null +++ b/spec/models/route_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Route, models: true do + let!(:group) { create(:group) } + let!(:route) { group.route } + + describe 'relationships' do + it { is_expected.to belong_to(:source) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_uniqueness_of(:path) } + end + + describe '#rename_children' do + let!(:nested_group) { create(:group, path: "test", parent: group) } + let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) } + + it "updates children routes with new path" do + route.update_attributes(path: 'bar') + + expect(described_class.exists?(path: 'bar')).to be_truthy + expect(described_class.exists?(path: 'bar/test')).to be_truthy + expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0b89ac7960e..75b270aa93c 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -234,11 +234,14 @@ describe API::MergeRequests, api: true do target_branch: 'master', author: user, labels: 'label, label2', - milestone_id: milestone.id + milestone_id: milestone.id, + remove_source_branch: true + expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(['label', 'label2']) expect(json_response['milestone']['id']).to eq(milestone.id) + expect(json_response['force_remove_source_branch']).to be_truthy end it "returns 422 when source_branch equals target_branch" do @@ -511,6 +514,13 @@ describe API::MergeRequests, api: true do expect(json_response['target_branch']).to eq('wiki') end + it "returns merge_request that removes the source branch" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + + expect(response).to have_http_status(200) + expect(json_response['force_remove_source_branch']).to be_truthy + end + it 'allows special label names' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: 'new issue', diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index bc340ff9d3c..7e3705983fb 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -126,17 +126,27 @@ describe MergeRequests::RefreshService, services: true do end context 'push to fork repo target branch' do - before do - service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') - reload_mrs + describe 'changes to merge requests' do + before do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + describe 'merge request diff' do + it 'does not reload the diff of the merge request made from fork' do + expect do + service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + end.not_to change { @fork_merge_request.reload.merge_request_diff } + end + end end context 'push to origin repo target branch after fork project was removed' do |