summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2018-05-17 15:40:39 +0100
committerFilipa Lacerda <filipa@gitlab.com>2018-05-17 15:40:39 +0100
commit49673de34f326a15d3358cb4ff22a73dfa5d4b20 (patch)
tree456d5744b1afb95ce12d7a2364f2dd7842b88a0d
parent9e61d26c35acd6e60ac0fb0df711dbfdfda7c448 (diff)
parentd9b78477000dafc4f8b8fd7e795e97649b8c6718 (diff)
downloadgitlab-ce-46381-dropdown-mr-widget.tar.gz
Merge branch 'master' into 46381-dropdown-mr-widget46381-dropdown-mr-widget
* master: (40 commits) Add changelog Update quick_start_guide.md Resolve "Opening Project with invite but without accepting leads to 404 error page" Respect the inheritance chain between Ci::Build and CommitStatus Remove unneccessary imports fixed copy to cliboard button in embedded snippets Fix Error 500 viewing admin page due to statement timeouts Grant privileges after database is created Only setup db in the first checkout! Project Sidebar: Split CI/CD into CI/CD and Operations Fix GPM content types for Doorkeeper Workhorse to send raw diff and patch for commits Refactor out duplication in runner_policy.rb Remove unnecessary runner.is_shared? checks in api because they are handled by policy Allow admin to assign shared runner to project through API Change policy list_runner_jobs -> read_runner Rename User#ci_authorized_runners -> ci_owned_runners Improve efficiency of authorized_runner policy query Use can? policies for lib/api/runners.rb Allow group runners to be viewed/edited in API ...
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--LICENSE7
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue35
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js54
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue4
-rw-r--r--app/assets/stylesheets/framework/snippets.scss3
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss13
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/concerns/accepts_pending_invitations.rb15
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/projects/commit_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/helpers/count_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/models/appearance.rb3
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/with_uploads.rb39
-rw-r--r--app/models/group.rb3
-rw-r--r--app/models/list.rb3
-rw-r--r--app/models/project.rb3
-rw-r--r--app/models/user.rb32
-rw-r--r--app/policies/ci/runner_policy.rb15
-rw-r--r--app/presenters/ci/build_presenter.rb25
-rw-r--r--app/presenters/commit_status_presenter.rb24
-rw-r--r--app/presenters/generic_commit_status_presenter.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml20
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml116
-rw-r--r--app/views/notify/member_invited_email.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml5
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--changelogs/unreleased/22647-width-contributors-graphs.yml5
-rw-r--r--changelogs/unreleased/42531-open-invite-404.yml5
-rw-r--r--changelogs/unreleased/43673-operations-tab-mvc.yml5
-rw-r--r--changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml5
-rw-r--r--changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml5
-rw-r--r--changelogs/unreleased/jivl-add-dot-system-notes.yml5
-rw-r--r--changelogs/unreleased/jprovazn-remote-upload-destroy.yml5
-rw-r--r--changelogs/unreleased/move-disussion-actions-to-the-right.yml5
-rw-r--r--changelogs/unreleased/zj-add-branch-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-commit-patch-diff.yml5
-rw-r--r--doc/topics/autodevops/quick_start_guide.md4
-rw-r--r--lib/api/groups.rb1
-rw-r--r--lib/api/runners.rb23
-rw-r--r--lib/api/v3/groups.rb1
-rw-r--r--lib/api/v3/runners.rb2
-rw-r--r--lib/gitlab/database/count.rb48
-rw-r--r--lib/gitlab/git/commit.rb25
-rw-r--r--lib/gitlab/git/repository.rb26
-rw-r--r--lib/gitlab/metrics/web_transaction.rb6
-rw-r--r--locale/gitlab.pot170
-rw-r--r--scripts/create_mysql_user.sh1
-rw-r--r--scripts/create_postgres_user.sh4
-rw-r--r--scripts/prepare_build.sh18
-rw-r--r--scripts/utils.sh18
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb33
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb6
-rw-r--r--spec/features/invites_spec.rb112
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/lib/gitlab/database/count_spec.rb62
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb14
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb6
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/models/appearance_spec.rb10
-rw-r--r--spec/models/ci/runner_spec.rb62
-rw-r--r--spec/models/commit_spec.rb1
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/generic_commit_status_spec.rb6
-rw-r--r--spec/models/group_spec.rb10
-rw-r--r--spec/models/project_spec.rb10
-rw-r--r--spec/models/repository_spec.rb68
-rw-r--r--spec/models/user_spec.rb100
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb2
-rw-r--r--spec/presenters/commit_status_presenter_spec.rb15
-rw-r--r--spec/requests/api/runners_spec.rb13
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb23
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb9
85 files changed, 980 insertions, 471 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b1445feee58..84d8e69b84e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- - git checkout $CI_COMMIT_SHA
+ - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
diff --git a/LICENSE b/LICENSE
index a42e07dfe91..a76372fad2c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -4,9 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
----
-
-All Documentation content that resides under the doc/ directory of this
-repository is licensed under Creative Commons: CC BY-SA 4.0.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
index 9c1aad65307..013cac75c46 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,8 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
+All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
+
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c2a35341eb2..fae580c091b 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -179,7 +179,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ class="alert alert-danger alert-block append-bottom-0"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 4ddca918495..2dc39d1a186 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -32,17 +32,17 @@ export default {
<template>
<div :class="className">
{{ actionText }}
- <time-ago-tooltip
- :time="editedAt"
- tooltip-placement="bottom"
- />
<template v-if="editedBy">
- by
+ {{ s__('ByAuthor|by') }}
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ <time-ago-tooltip
+ :time="editedAt"
+ tooltip-placement="bottom"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index c3d1ef1fcc6..a4081957207 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -62,6 +62,21 @@ export default {
<template>
<div class="note-header-info">
+ <div
+ v-if="includeToggle"
+ class="discussion-actions">
+ <button
+ @click="handleToggle"
+ class="note-action-button discussion-toggle-button js-vue-toggle-button"
+ type="button">
+ <i
+ :class="toggleChevronClass"
+ class="fa"
+ aria-hidden="true">
+ </i>
+ {{ __('Toggle discussion') }}
+ </button>
+ </div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
@@ -78,10 +93,13 @@ export default {
v-html="actionTextHtml"
class="system-note-message">
</span>
+ <span class="system-note-separator">
+ &middot;
+ </span>
<a
:href="noteTimestampLink"
@click="updateTargetNoteHash"
- class="note-timestamp">
+ class="note-timestamp system-note-separator">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
@@ -95,20 +113,5 @@ export default {
</i>
</span>
</span>
- <div
- v-if="includeToggle"
- class="discussion-actions">
- <button
- @click="handleToggle"
- class="note-action-button discussion-toggle-button js-vue-toggle-button"
- type="button">
- <i
- :class="toggleChevronClass"
- class="fa"
- aria-hidden="true">
- </i>
- Toggle discussion
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
index a99ce0f1c36..5316d3e9f3c 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
-const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
+const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = {
top: 20,
- right: 20,
+ right: 10,
bottom: 30,
- left: 50
+ left: 40
};
ContributorsGraph.prototype.x_domain = null;
@@ -32,6 +32,12 @@ export const ContributorsGraph = (function() {
ContributorsGraph.prototype.dates = [];
+ ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
+ const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
+ const marginWidth = this.MARGIN.left + this.MARGIN.right;
+ return baseWidth - parentPaddingWidth - marginWidth;
+ };
+
ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data;
};
@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master');
- const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1;
this.update_content = this.update_content.bind(this);
- this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
this.height = 200;
this.x = null;
this.y = null;
@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}
ContributorsMasterGraph.prototype.process_dates = function(data) {
- var dates;
- dates = this.get_dates(data);
+ const dates = this.get_dates(data);
this.parse_dates(data);
return ContributorsGraph.set_dates(dates);
};
@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_svg = function() {
- return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3.select("#contributors-master")
+ .append("svg")
+ .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr("class", "tint-box")
+ .append("g")
+ .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ return this.svg;
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) {
+ const $parentElements = $('.person');
+
this.data = data1;
// Don't split graph size in half for mobile devices.
- if ($(window).width() < 768) {
- this.width = $('.content').width() - 80;
+ if ($(window).width() < 790) {
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else {
- this.width = ($('.content').width() / 2) - 100;
+ this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
}
this.height = 200;
this.x = null;
@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- var persons = document.querySelectorAll('.person');
+ const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
- return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ this.svg = d3.select(this.list_item)
+ .append("svg")
+ .attr("width", this.width + this.MARGIN.left + this.MARGIN.right)
+ .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom)
+ .attr("class", "spark")
+ .append("g")
+ .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ return this.svg;
};
ContributorsAuthorGraph.prototype.draw_path = function(data) {
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index bec4e7c99b6..368eeb6c453 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -40,7 +40,7 @@ export default {
:class="cssClass"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
- data-container="body">
- {{ timeFormated(time) }}
+ data-container="body"
+ v-text="timeFormated(time)">
</time>
</template>
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 606d4675f19..430633bb01b 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -67,7 +67,8 @@
padding: 8px 40px;
}
- .embed-toggle {
+ .embed-toggle,
+ .snippet-clipboard-btn {
height: 35px;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 011d38532b4..6bb40bae9ed 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,5 +1,3 @@
-@import './issues/issue_count_badge';
-
[v-cloak] {
display: none;
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b9390450477..0d17b9bae7e 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,5 +1,3 @@
-@import "./issues/issue_count_badge";
-
.issues-list {
.issue {
padding: 10px 0 10px $gl-padding;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 6d5c6cb136f..feee964f9bb 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -407,10 +407,6 @@ ul.notes {
.note-header {
display: flex;
justify-content: space-between;
-
- @include notes-media('max', $screen-xs-max) {
- flex-flow: row wrap;
- }
}
.note-header-info {
@@ -459,6 +455,10 @@ ul.notes {
white-space: normal;
}
+ .system-note-separator {
+ color: $gl-text-color-disabled;
+ }
+
a:hover {
text-decoration: underline;
}
@@ -473,11 +473,6 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @include notes-media('max', $screen-md-max) {
- float: none;
- margin-left: 0;
- }
-
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index e85cdcb8db7..d6a6bc7d4a1 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,4 +1,6 @@
class Admin::DashboardController < Admin::ApplicationController
+ include CountHelper
+
def index
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb
new file mode 100644
index 00000000000..6e8aef52b52
--- /dev/null
+++ b/app/controllers/concerns/accepts_pending_invitations.rb
@@ -0,0 +1,15 @@
+module AcceptsPendingInvitations
+ extend ActiveSupport::Concern
+
+ def accept_pending_invitations
+ return unless resource.active_for_authentication?
+
+ clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
+ end
+
+ def clear_stored_location_for_resource
+ session_key = stored_location_key_for(resource)
+
+ session.delete(session_key)
+ end
+end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 6d9c38d9581..7bc46a6ccc0 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -1,4 +1,6 @@
class ConfirmationsController < Devise::ConfirmationsController
+ include AcceptsPendingInvitations
+
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(resource_name, resource)
+ accept_pending_invitations
+
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index b7f548e0e63..1d1184d46d1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format|
format.html { render }
- format.diff { render text: @commit.to_diff }
- format.patch { render text: @commit.to_patch }
+ format.diff do
+ send_git_diff(@project.repository, @commit.diff_refs)
+ end
+ format.patch do
+ send_git_patch(@project.repository, @commit.diff_refs)
+ end
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 177c8a54099..1d850baf012 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -69,7 +69,7 @@ module Projects
@project_runners = @project.runners.ordered
@assignable_runners = current_user
- .ci_authorized_runners
+ .ci_owned_runners
.assignable_for(project)
.ordered
.page(params[:page]).per(20)
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 1848c806c41..f5a222b3a48 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,6 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
+ include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
+ accept_pending_invitations
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
- user.confirmed? ? dashboard_projects_path : users_almost_there_path
+ user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
new file mode 100644
index 00000000000..24ee62e68ba
--- /dev/null
+++ b/app/helpers/count_helper.rb
@@ -0,0 +1,5 @@
+module CountHelper
+ def approximate_count_with_delimiters(model)
+ number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index eb81dc2de43..fa54eafd3a3 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -257,6 +257,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
+ nav_tabs << :operations
end
if project.external_issue_tracker
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index fb66dd0b766..f8713138a93 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -2,6 +2,7 @@ class Appearance < ActiveRecord::Base
include CacheMarkdownField
include AfterCommitQueue
include ObjectStorage::BackgroundMove
+ include WithUploads
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
@@ -14,8 +15,6 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
after_commit :flush_redis_cache
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index bda69f85a78..e6f1ed519be 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -52,7 +52,7 @@ module Ci
# Without that, placeholders would miss one and couldn't match.
where(locked: false)
.where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
- .specific
+ .project_type
end
validate :tag_constraints
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 97d89422594..a7d05722287 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
+ include Presentable
self.table_name = 'ci_builds'
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
new file mode 100644
index 00000000000..e7cfffb775b
--- /dev/null
+++ b/app/models/concerns/with_uploads.rb
@@ -0,0 +1,39 @@
+# Mounted uploaders are destroyed by carrierwave's after_commit
+# hook. This hook fetches upload location (local vs remote) from
+# Upload model. So it's neccessary to make sure that during that
+# after_commit hook model's associated uploads are not deleted yet.
+# IOW we can not use dependent: :destroy :
+# has_many :uploads, as: :model, dependent: :destroy
+#
+# And because not-mounted uploads require presence of upload's
+# object model when destroying them (FileUploader's `build_upload` method
+# references `model` on delete), we can not use after_commit hook for these
+# uploads.
+#
+# Instead FileUploads are destroyed in before_destroy hook and remaining uploads
+# are destroyed by the carrierwave's after_commit hook.
+
+module WithUploads
+ extend ActiveSupport::Concern
+
+ # Currently there is no simple way how to select only not-mounted
+ # uploads, it should be all FileUploaders so we select them by
+ # `uploader` class
+ FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze
+
+ included do
+ has_many :uploads, as: :model
+
+ before_destroy :destroy_file_uploads
+ end
+
+ # mounted uploads are deleted in carrierwave's after_commit hook,
+ # but FileUploaders which are not mounted must be deleted explicitly and
+ # it can not be done in after_commit because FileUploader requires loads
+ # associated model on destroy (which is already deleted in after_commit)
+ def destroy_file_uploads
+ self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload|
+ upload.destroy
+ end
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index cefca316399..8fb77a7869d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -10,6 +10,7 @@ class Group < Namespace
include LoadedInGroupList
include GroupDescendant
include TokenAuthenticatable
+ include WithUploads
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -30,8 +31,6 @@ class Group < Namespace
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
has_many :boards
has_many :badges, class_name: 'GroupBadge'
diff --git a/app/models/list.rb b/app/models/list.rb
index 918275be142..5daf35ef845 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
- only: [:id, :title, :description, :color]
+ only: [:id, :title, :description, :color],
+ methods: [:text_color]
)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 534a0e630af..0975e64e995 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -23,6 +23,7 @@ class Project < ActiveRecord::Base
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
include FastDestroyAll::Helpers
+ include WithUploads
extend Gitlab::ConfigHelper
@@ -301,8 +302,6 @@ class Project < ActiveRecord::Base
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope }
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
diff --git a/app/models/user.rb b/app/models/user.rb
index 173ab38e20c..8ef3c3ceff0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include IgnorableColumn
include BulkMemberAccessLoad
include BlocksJsonSerialization
+ include WithUploads
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -137,7 +138,6 @@ class User < ActiveRecord::Base
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email?
end
+ def accept_pending_invitations!
+ pending_invitations.select do |member|
+ member.accept_invite!(self)
+ end
+ end
+
+ def pending_invitations
+ Member.where(invite_email: verified_emails).invite
+ end
+
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
@@ -999,12 +1009,19 @@ class User < ActiveRecord::Base
!solo_owned_groups.present?
end
- def ci_authorized_runners
- @ci_authorized_runners ||= begin
- runner_ids = Ci::RunnerProject
+ def ci_owned_runners
+ @ci_owned_runners ||= begin
+ project_runner_ids = Ci::RunnerProject
.where(project: authorized_projects(Gitlab::Access::MASTER))
.select(:runner_id)
- Ci::Runner.specific.where(id: runner_ids)
+
+ group_runner_ids = Ci::RunnerNamespace
+ .where(namespace_id: owned_or_masters_groups.select(:id))
+ .select(:runner_id)
+
+ union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
+
+ Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
@@ -1205,6 +1222,11 @@ class User < ActiveRecord::Base
!terms_accepted?
end
+ def owned_or_masters_groups
+ union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
+ Group.from("(#{union.to_sql}) namespaces")
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
index 7dff8470e23..895abe87d86 100644
--- a/app/policies/ci/runner_policy.rb
+++ b/app/policies/ci/runner_policy.rb
@@ -1,16 +1,19 @@
module Ci
class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0
- condition(:shared) { @subject.is_shared? }
-
- with_options scope: :subject, score: 0
condition(:locked, scope: :subject) { @subject.locked? }
- condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) }
+ condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) }
rule { anonymous }.prevent_all
- rule { admin | authorized_runner }.enable :assign_runner
- rule { ~admin & shared }.prevent :assign_runner
+
+ rule { admin | owned_runner }.policy do
+ enable :assign_runner
+ enable :read_runner
+ enable :update_runner
+ enable :delete_runner
+ end
+
rule { ~admin & locked }.prevent :assign_runner
end
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 4873d7ce662..e0aaa5cb736 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,16 +1,5 @@
module Ci
- class BuildPresenter < Gitlab::View::Presenter::Delegated
- CALLOUT_FAILURE_MESSAGES = {
- unknown_failure: 'There is an unknown failure, please try again',
- script_failure: 'There has been a script failure. Check the job log for more information',
- api_failure: 'There has been an API failure, please try again',
- stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
- runner_system_failure: 'There has been a runner system failure, please try again',
- missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
- }.freeze
-
- presents :build
-
+ class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
- def callout_failure_message
- CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
- end
-
- def recoverable?
- failed? && !unrecoverable?
- end
-
private
def tooltip_for_badge
@@ -61,9 +42,5 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
-
- def unrecoverable?
- script_failure? || missing_dependency_failure?
- end
end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
new file mode 100644
index 00000000000..c7f7aa836bd
--- /dev/null
+++ b/app/presenters/commit_status_presenter.rb
@@ -0,0 +1,24 @@
+class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
+ presents :build
+
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
+end
diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb
new file mode 100644
index 00000000000..da09df29a37
--- /dev/null
+++ b/app/presenters/generic_commit_status_presenter.rb
@@ -0,0 +1,2 @@
+class GenericCommitStatusPresenter < CommitStatusPresenter
+end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index bbf0e0fb95c..41ef646fc0e 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -10,7 +10,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
- = number_with_delimiter(Project.cached_count)
+ = approximate_count_with_delimiters(Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -19,7 +19,7 @@
= link_to admin_users_path do
%h3.text-center
Users:
- = number_with_delimiter(User.count)
+ = approximate_count_with_delimiters(User)
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -28,7 +28,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
- = number_with_delimiter(Group.count)
+ = approximate_count_with_delimiters(Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
@@ -39,31 +39,31 @@
%p
Forks
%span.light.pull-right
- = number_with_delimiter(ForkedProjectLink.count)
+ = approximate_count_with_delimiters(ForkedProjectLink)
%p
Issues
%span.light.pull-right
- = number_with_delimiter(Issue.count)
+ = approximate_count_with_delimiters(Issue)
%p
Merge Requests
%span.light.pull-right
- = number_with_delimiter(MergeRequest.count)
+ = approximate_count_with_delimiters(MergeRequest)
%p
Notes
%span.light.pull-right
- = number_with_delimiter(Note.count)
+ = approximate_count_with_delimiters(Note)
%p
Snippets
%span.light.pull-right
- = number_with_delimiter(Snippet.count)
+ = approximate_count_with_delimiters(Snippet)
%p
SSH Keys
%span.light.pull-right
- = number_with_delimiter(Key.count)
+ = approximate_count_with_delimiters(Key)
%p
Milestones
%span.light.pull-right
- = number_with_delimiter(Milestone.count)
+ = approximate_count_with_delimiters(Milestone)
%p
Active Users
%span.light.pull-right
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index e9589213f80..ebe8c327079 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -13,7 +13,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
- Toggle discussion
+ = _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 196db08cebd..c3ea592a6b5 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,13 +13,13 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
- Project
+ = _('Project')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Overview') }
+ = _('Overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
@@ -40,45 +40,45 @@
.nav-icon-container
= sprite_icon('doc_text')
%span.nav-item-name
- Repository
+ = _('Repository')
%ul.sidebar-sub-level-items
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Repository') }
+ = _('Repository')
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
- #{ _('Files') }
+ = _('Files')
= nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref) do
- #{ _('Commits') }
+ = _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project) do
- #{ _('Branches') }
+ = _('Branches')
= nav_link(controller: [:tags, :releases]) do
= link_to project_tags_path(@project) do
- #{ _('Tags') }
+ = _('Tags')
= nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do
- #{ _('Contributors') }
+ = _('Contributors')
= nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do
- #{ s_('ProjectNetworkGraph|Graph') }
+ = _('Graph')
= nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
- #{ _('Compare') }
+ = _('Compare')
= nav_link(path: 'graphs#charts') do
= link_to charts_project_graph_path(@project, current_ref) do
- #{ _('Charts') }
+ = _('Charts')
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
@@ -86,7 +86,7 @@
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
- Issues
+ = _('Issues')
- if @project.issues_enabled?
%span.badge.count.issue_counter
= number_with_delimiter(@project.open_issues_count)
@@ -95,7 +95,7 @@
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Issues') }
+ = _('Issues')
- if @project.issues_enabled?
%span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count)
@@ -103,7 +103,7 @@
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
- List
+ = _('List')
= nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text do
@@ -113,12 +113,12 @@
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
%span
- Labels
+ = _('Labels')
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
- Milestones
+ = _('Milestones')
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
@@ -139,54 +139,75 @@
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
- Merge Requests
+ = _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Merge Requests') }
+ = _('Merge Requests')
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= sprite_icon('pipeline')
%span.nav-item-name
- CI / CD
+ = _('CI / CD')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
- #{ _('CI / CD') }
+ = _('CI / CD')
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
- Pipelines
+ = _('Pipelines')
- if project_nav_tab? :builds
= nav_link(controller: [:jobs, :artifacts]) do
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
- Jobs
+ = _('Jobs')
- if project_nav_tab? :pipelines
= nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do
%span
- Schedules
+ = _('Schedules')
+
+ - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
+ = nav_link(path: 'pipelines#charts') do
+ = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
+ %span
+ = _('Charts')
+
+ - if project_nav_tab? :operations
+ = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
+ = link_to project_environments_path(@project), class: 'shortcuts-operations' do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Operations')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_environments_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Operations')
+ %li.divider.fly-out-top-item
- if project_nav_tab? :environments
= nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
- Environments
+ = _('Environments')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
@@ -217,19 +238,18 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
- - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
- = nav_link(path: 'pipelines#charts') do
- = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
- %span
- Charts
-
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= sprite_icon('disk')
%span.nav-item-name
- Registry
+ = _('Registry')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_container_registry_index_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Registry')
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
@@ -237,12 +257,12 @@
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
- Wiki
+ = _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
= link_to get_project_wiki_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Wiki') }
+ = _('Wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
@@ -250,12 +270,12 @@
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
- Snippets
+ = _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Snippets') }
+ = _('Snippets')
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
@@ -263,7 +283,7 @@
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item
- Settings
+ = _('Settings')
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
@@ -271,16 +291,16 @@
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Settings') }
+ = _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do
%span
- General
+ = _('General')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members' do
%span
- Members
+ = _('Members')
- if can_edit
= nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do
@@ -290,21 +310,21 @@
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
- Integrations
+ = _('Integrations')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: 'Repository' do
%span
- Repository
+ = _('Repository')
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span
- CI / CD
+ = _('CI / CD')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do
%span
- Pages
+ = _('Pages')
- else
= nav_link(controller: :project_members) do
@@ -312,12 +332,12 @@
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
- Members
+ = _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Members') }
+ = _('Members')
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index b8b75da3f2f..6730172242b 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -4,7 +4,7 @@
by
= link_to member.created_by.name, user_url(member.created_by)
to join the
- = link_to member_source.human_name, member_source.web_url
+ = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
#{member_source.model_name.singular} as #{member.human_access}.
%p
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 4bff6468bb0..aea40df41b0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -15,7 +15,7 @@
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
class: "label color-label title board-title-text",
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" }
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
- if can?(current_user, :admin_list, current_board_parent)
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 893a7f26ebd..d4e67b5e7e3 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -41,8 +41,9 @@
- if note.system
%span.system-note-message
= markdown_field(note, :note)
- %a{ href: "##{dom_id(note)}" }
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ %span.system-note-separator
+ &middot;
+ %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- if note.for_personal_snippet?
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 836230ae8ee..9f55c10d19b 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn
- %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
+ %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' }
= sprite_icon('duplicate', size: 16)
.clearfix
diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml
new file mode 100644
index 00000000000..87be3a25d8a
--- /dev/null
+++ b/changelogs/unreleased/22647-width-contributors-graphs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix width of contributors graphs
+merge_request: 18639
+author: Paul Vorbach
+type: fixed
diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml
new file mode 100644
index 00000000000..73729f4a929
--- /dev/null
+++ b/changelogs/unreleased/42531-open-invite-404.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically accepts project/group invite by email after user signup
+merge_request: 17634
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/43673-operations-tab-mvc.yml b/changelogs/unreleased/43673-operations-tab-mvc.yml
new file mode 100644
index 00000000000..cd580e7a8d6
--- /dev/null
+++ b/changelogs/unreleased/43673-operations-tab-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
+merge_request: 18941
+author:
+type: changed
diff --git a/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml
new file mode 100644
index 00000000000..2f885c5c927
--- /dev/null
+++ b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml
@@ -0,0 +1,5 @@
+---
+title: Allow CommitStatus class to use presentable methods
+merge_request: 18979
+author:
+type: fixed
diff --git a/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml b/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml
new file mode 100644
index 00000000000..c8cdf3672b3
--- /dev/null
+++ b/changelogs/unreleased/46303_copy_button_fix_embedded_snippets.yml
@@ -0,0 +1,5 @@
+---
+title: fixed copy to blipboard button in embed bar of snippets
+merge_request: 18923
+author: haseebeqx
+type: fixed
diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml
new file mode 100644
index 00000000000..2246bab1464
--- /dev/null
+++ b/changelogs/unreleased/jivl-add-dot-system-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Add dot to separate system notes content
+merge_request: 18864
+author:
+type: changed
diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
new file mode 100644
index 00000000000..22e55920fa3
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
@@ -0,0 +1,5 @@
+---
+title: Fix deletion of Object Store uploads
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-disussion-actions-to-the-right.yml b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
new file mode 100644
index 00000000000..b79c6f36585
--- /dev/null
+++ b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
@@ -0,0 +1,5 @@
+---
+title: Move discussion actions to the right for small viewports
+merge_request: 18476
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/zj-add-branch-mandatory.yml b/changelogs/unreleased/zj-add-branch-mandatory.yml
new file mode 100644
index 00000000000..82712ce842d
--- /dev/null
+++ b/changelogs/unreleased/zj-add-branch-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Adding branches through the WebUI is handled by Gitaly
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
new file mode 100644
index 00000000000..bce68692d98
--- /dev/null
+++ b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Workhorse to send raw diff and patch for commits
+merge_request:
+author:
+type: other
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 15567715c98..0b16af2953b 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -23,6 +23,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
+You can also start a new project from a
+[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
+you want to use a different language.
+
## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 92e3d5cc10a..0d125cd7831 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -165,6 +165,7 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).execute
end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 5f2a9567605..5cb96d467c0 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
- runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
+ runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner
end
@@ -184,40 +184,35 @@ module API
def authenticate_show_runner!(runner)
return if runner.is_shared || current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
def authenticate_update_runner!(runner)
return if current_user.admin?
- forbidden!("Runner is shared") if runner.is_shared?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
end
def authenticate_delete_runner!(runner)
return if current_user.admin?
- forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
end
def authenticate_enable_runner!(runner)
- forbidden!("Runner is shared") if runner.is_shared?
- forbidden!("Runner is locked") if runner.locked?
+ forbidden!("Runner is a group runner") if runner.group_type?
+
return if current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("Runner is locked") if runner.locked?
+ forbidden!("No access granted") unless can?(current_user, :assign_runner, runner)
end
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
- end
-
- def user_can_access_runner?(runner)
- current_user.ci_authorized_runners.exists?(runner.id)
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
end
end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 2c52d21fa1c..3844fd4810d 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -131,6 +131,7 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
end
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
index c6d9957d452..8a5c46805bd 100644
--- a/lib/api/v3/runners.rb
+++ b/lib/api/v3/runners.rb
@@ -58,7 +58,7 @@ module API
end
def user_can_access_runner?(runner)
- current_user.ci_authorized_runners.exists?(runner.id)
+ current_user.ci_owned_runners.exists?(runner.id)
end
end
end
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
new file mode 100644
index 00000000000..3374203960e
--- /dev/null
+++ b/lib/gitlab/database/count.rb
@@ -0,0 +1,48 @@
+# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
+# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
+module Gitlab
+ module Database
+ module Count
+ CONNECTION_ERRORS =
+ if defined?(PG)
+ [
+ ActionView::Template::Error,
+ ActiveRecord::StatementInvalid,
+ PG::Error
+ ].freeze
+ else
+ [
+ ActionView::Template::Error,
+ ActiveRecord::StatementInvalid
+ ].freeze
+ end
+
+ def self.approximate_count(model)
+ return model.count unless Gitlab::Database.postgresql?
+
+ execute_estimate_if_updated_recently(model) || model.count
+ end
+
+ def self.execute_estimate_if_updated_recently(model)
+ ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model)
+ rescue *CONNECTION_ERRORS
+ end
+
+ def self.reltuples_updated_recently?(model)
+ time = "to_timestamp(#{1.hour.ago.to_i})"
+ query = <<~SQL
+ SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND
+ (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
+ SQL
+
+ ActiveRecord::Base.connection.select_all(query).count > 0
+ rescue *CONNECTION_ERRORS
+ false
+ end
+
+ def self.postgresql_estimate_query(model)
+ "SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index fabcd46c8e9..d79a4dbeee4 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -342,21 +342,6 @@ module Gitlab
parent_ids.first
end
- # Shows the diff between the commit's parent and the commit.
- #
- # Cuts out the header and stats from #to_patch and returns only the diff.
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
- def to_diff
- Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.patch(id)
- else
- rugged_diff_from_parent.patch
- end
- end
- end
-
# Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an
# empty repo. See Repository#diff for keys allowed in the +options+
@@ -432,16 +417,6 @@ module Gitlab
Gitlab::Git::CommitStats.new(@repository, self)
end
- def to_patch(options = {})
- begin
- rugged_commit.to_mbox(options)
- rescue Rugged::InvalidError => ex
- if ex.message =~ /commit \w+ is a merge commit/i
- 'Patch format is not currently supported for merge commits.'
- end
- end
- end
-
# Get ref names collection
#
# Ex.
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 25487f53999..061865a7acf 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -776,13 +776,9 @@ module Gitlab
end
def add_branch(branch_name, user:, target:)
- gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_add_branch(branch_name, user, target)
- else
- rugged_add_branch(branch_name, user, target)
- end
- end
+ gitaly_operation_client.user_create_branch(branch_name, user, target)
+ rescue GRPC::FailedPrecondition => ex
+ raise InvalidRef, ex
end
def add_tag(tag_name, user:, target:, message: nil)
@@ -2197,22 +2193,6 @@ module Gitlab
end
end
- def gitaly_add_branch(branch_name, user, target)
- gitaly_operation_client.user_create_branch(branch_name, user, target)
- rescue GRPC::FailedPrecondition => ex
- raise InvalidRef, ex
- end
-
- def rugged_add_branch(branch_name, user, target)
- target_object = Ref.dereference_object(lookup(target))
- raise InvalidRef.new("target not found: #{target}") unless target_object
-
- OperationService.new(user, self).add_branch(branch_name, target_object.oid)
- find_branch(branch_name)
- rescue Rugged::ReferenceError => ex
- raise InvalidRef, ex
- end
-
def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 7cf33ca9e8a..3799aaebf1c 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -28,7 +28,11 @@ module Gitlab
controller = @env[CONTROLLER_KEY]
action = "#{controller.action_name}"
- suffix = controller.request_format
+
+ # Devise exposes a method called "request_format" that does the below.
+ # However, this method is not available to all controllers (e.g. certain
+ # Doorkeeper controllers). As such we use the underlying code directly.
+ suffix = controller.request.format.try(:ref)
if suffix && suffix != :html
action += ".#{suffix}"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1179f71353a..90cdfd0dd03 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-14 10:49+0200\n"
-"PO-Revision-Date: 2018-05-14 10:49+0200\n"
+"POT-Creation-Date: 2018-05-15 15:05+0200\n"
+"PO-Revision-Date: 2018-05-15 15:05+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -53,6 +53,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
@@ -101,6 +111,9 @@ msgstr ""
msgid "%{title} changes"
msgstr ""
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
@@ -207,6 +220,9 @@ msgstr ""
msgid "Active"
msgstr ""
+msgid "Active Sessions"
+msgstr ""
+
msgid "Activity"
msgstr ""
@@ -315,6 +331,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr ""
+
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
@@ -1022,6 +1041,9 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -1145,6 +1167,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1235,6 +1260,9 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
msgid "Collapse"
msgstr ""
@@ -1359,6 +1387,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1524,6 +1555,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
+msgid "Created"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -1595,6 +1629,54 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
@@ -1751,9 +1833,6 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
-msgstr ""
-
msgid "Email"
msgstr ""
@@ -1793,6 +1872,9 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
+msgid "Environments"
+msgstr ""
+
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
@@ -2005,6 +2087,9 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
+msgid "General"
+msgstr ""
+
msgid "Generate a default set of labels"
msgstr ""
@@ -2053,6 +2138,9 @@ msgstr ""
msgid "Got it!"
msgstr ""
+msgid "Graph"
+msgstr ""
+
msgid "Group CI/CD settings"
msgstr ""
@@ -2166,6 +2254,18 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
@@ -2199,6 +2299,9 @@ msgstr ""
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
+msgid "Integrations"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2336,6 +2439,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2360,6 +2466,9 @@ msgstr ""
msgid "Leave project"
msgstr ""
+msgid "List"
+msgstr ""
+
msgid "List your GitHub repositories"
msgstr ""
@@ -2462,6 +2571,9 @@ msgstr ""
msgid "Milestone"
msgstr ""
+msgid "Milestones"
+msgstr ""
+
msgid "Milestones|Delete milestone"
msgstr ""
@@ -2719,6 +2831,9 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
@@ -2878,25 +2993,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Existing branch name, tag"
+msgid "Pipeline|Create for"
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Existing branch name or tag"
msgstr ""
msgid "Pipeline|Run Pipeline"
msgstr ""
-msgid "Pipeline|Run on"
-msgstr ""
-
-msgid "Pipeline|Run pipeline"
+msgid "Pipeline|Search branches"
msgstr ""
-msgid "Pipeline|Search branches"
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -2905,7 +3017,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -3022,6 +3134,9 @@ msgstr ""
msgid "Progress"
msgstr ""
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -3073,9 +3188,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr ""
-msgid "ProjectNetworkGraph|Graph"
-msgstr ""
-
msgid "Projects"
msgstr ""
@@ -3226,6 +3338,9 @@ msgstr ""
msgid "Register and see your runners for this group."
msgstr ""
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr ""
@@ -3268,6 +3383,9 @@ msgstr ""
msgid "Repository maintenance"
msgstr ""
+msgid "Repository mirror settings"
+msgstr ""
+
msgid "Repository storage"
msgstr ""
@@ -3363,6 +3481,9 @@ msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
@@ -3405,6 +3526,9 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
@@ -3937,6 +4061,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
@@ -4253,9 +4380,6 @@ msgstr ""
msgid "Verified"
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
msgid "View file @ "
msgstr ""
@@ -4373,6 +4497,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr ""
+
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
msgstr ""
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
index 286b1325f1d..35f68c581f3 100644
--- a/scripts/create_mysql_user.sh
+++ b/scripts/create_mysql_user.sh
@@ -1,7 +1,6 @@
#!/bin/bash
mysql --user=root --host=mysql <<EOF
-CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
index 8a744df3226..8a049bcd7fb 100644
--- a/scripts/create_postgres_user.sh
+++ b/scripts/create_postgres_user.sh
@@ -1,8 +1,6 @@
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
-DROP DATABASE IF EXISTS gitlabhq_test;
-CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
-GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 206d62dbc78..d8bcc9f8191 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
-# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
-# user but not necessarily a full schema loaded
-if [ "$CREATE_DB_USER" != "false" ]; then
- if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
- . scripts/create_postgres_user.sh
- else
- . scripts/create_mysql_user.sh
- fi
-fi
-
if [ "$SETUP_DB" != "false" ]; then
- bundle exec rake db:drop db:create db:schema:load db:migrate
-
- if [ "$GITLAB_DATABASE" = "mysql" ]; then
- bundle exec rake add_limits_mysql
- fi
+ setup_db
+elif getent hosts postgres || getent hosts mysql; then
+ setup_db_user_only
fi
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 6faa701f0ce..2d2ba115563 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -12,3 +12,21 @@ retry() {
done
return 1
}
+
+setup_db_user_only() {
+ if [ "$GITLAB_DATABASE" = "postgresql" ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+}
+
+setup_db() {
+ setup_db_user_only
+
+ bundle exec rake db:drop db:create db:schema:load db:migrate
+
+ if [ "$GITLAB_DATABASE" = "mysql" ]; then
+ bundle exec rake add_limits_mysql
+ fi
+}
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 694c64ae1ad..003fec8ac68 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -79,41 +79,18 @@ describe Projects::CommitController do
end
describe "as diff" do
- include_examples "export as", :diff
- let(:format) { :diff }
+ it "triggers workhorse to serve the request" do
+ go(id: commit.id, format: :diff)
- it "should really only be a git diff" do
- go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
-
- expect(response.body).to start_with("diff --git")
- end
-
- it "is only be a git diff without whitespace changes" do
- go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
-
- expect(response.body).to start_with("diff --git")
-
- # without whitespace option, there are more than 2 diff_splits for other formats
- diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
- expect(diff_splits.length).to be <= 2
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
describe "as patch" do
- include_examples "export as", :patch
- let(:format) { :patch }
- let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
-
- it "is a git email patch" do
- go(id: commit2.id, format: format)
-
- expect(response.body).to start_with("From #{commit2.id}")
- end
-
it "contains a git diff" do
- go(id: commit2.id, format: format)
+ go(id: commit.id, format: :patch)
- expect(response.body).to match(/^diff --git/)
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index a91c868cbaf..f1810763d2d 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do
end
context 'with group runners' do
- let(:group_runner) { create(:ci_runner) }
+ let(:group_runner) { create(:ci_runner, runner_type: :group_type) }
let(:parent_group) { create(:group) }
let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
let(:other_project) { create(:project, group: group) }
- let!(:project_runner) { create(:ci_runner, projects: [other_project]) }
+ let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) }
let!(:shared_runner) { create(:ci_runner, :shared) }
it 'sets assignable project runners only' do
@@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do
get :show, namespace_id: project.namespace, project_id: project
- expect(assigns(:assignable_runners)).to eq [project_runner]
+ expect(assigns(:assignable_runners)).to contain_exactly(project_runner)
end
end
end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index e4be6193b8b..a986ddc4abc 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
- let(:invite) { group.group_members.invite.last }
+ let(:group_invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
- invite.generate_invite_token!
+ group_invite.generate_invite_token!
+ end
+
+ def confirm_email_and_sign_in(new_user)
+ new_user_token = User.find_by_email(new_user.email).confirmation_token
+
+ visit user_confirmation_path(confirmation_token: new_user_token)
+ fill_in_sign_in_form(new_user)
+ end
+
+ def fill_in_sign_up_form(new_user)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ click_button "Register"
+ end
+
+ def fill_in_sign_in_form(user)
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: user.password
+ check 'user_remember_me'
+ click_button 'Sign in'
end
context 'when signed out' do
before do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
@@ -25,12 +48,9 @@ describe 'Invites' do
end
it 'sign in and redirects to invitation page' do
- fill_in 'user_login', with: user.email
- fill_in 'user_password', with: user.password
- check 'user_remember_me'
- click_button 'Sign in'
+ fill_in_sign_in_form(user)
- expect(current_path).to eq(invite_path(invite.raw_invite_token))
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
@@ -45,7 +65,7 @@ describe 'Invites' do
end
it 'shows message user already a member' do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.')
end
end
@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do
before do
- visit decline_invite_path(invite.raw_invite_token)
+ visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
@@ -94,4 +114,72 @@ describe 'Invites' do
end
end
end
+
+ describe 'invite an user using their email address' do
+ let(:new_user) { build_stubbed(:user) }
+ let(:invite_email) { new_user.email }
+ let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
+ let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
+
+ before do
+ stub_application_setting(send_user_confirmation_email: send_email_confirmation)
+ visit invite_path(group_invite.raw_invite_token)
+ end
+
+ context 'email confirmation disabled' do
+ let(:send_email_confirmation) { false }
+
+ it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+
+ context 'email confirmation enabled' do
+ let(:send_email_confirmation) { true }
+
+ it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ it "doesn't accept invitations until the user confirm his email" do
+ fill_in_sign_up_form(new_user)
+ sign_in(owner)
+
+ visit project_project_members_path(project)
+ expect(page).to have_content 'Invited'
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+ end
end
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 622a1e40d07..05922df6b81 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -17,6 +17,7 @@
"required": [
"id",
"color",
+ "text_color",
"description",
"title",
"priority"
@@ -29,6 +30,7 @@
},
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
+ "title": { "text_color": "string" },
"priority": { "type": ["integer", "null"] }
}
},
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
new file mode 100644
index 00000000000..9d9caaabe16
--- /dev/null
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count do
+ before do
+ create_list(:project, 3)
+ end
+
+ describe '.execute_estimate_if_updated_recently', :postgresql do
+ context 'when reltuples have not been updated' do
+ before do
+ expect(described_class).to receive(:reltuples_updated_recently?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil
+ end
+ end
+
+ context 'when reltuples have been updated' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ end
+
+ it 'calls postgresql_estimate_query' do
+ expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
+ expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3)
+ end
+ end
+ end
+
+ describe '.approximate_count' do
+ context 'when reltuples have not been updated' do
+ it 'counts all projects the normal way' do
+ allow(described_class).to receive(:reltuples_updated_recently?).and_return(false)
+
+ expect(Project).to receive(:count).and_call_original
+ expect(described_class.approximate_count(Project)).to eq(3)
+ end
+ end
+
+ context 'no permission' do
+ it 'falls back to standard query' do
+ allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege)
+
+ expect(Project).to receive(:count).and_call_original
+ expect(described_class.approximate_count(Project)).to eq(3)
+ end
+ end
+
+ describe 'when reltuples have been updated', :postgresql do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ end
+
+ it 'counts all projects in the fast way' do
+ expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
+
+ expect(described_class.approximate_count(Project)).to eq(3)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index a05feaac1ca..2e068584c2e 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -554,24 +554,10 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '#stats'
end
- describe '#to_diff' do
- subject { commit.to_diff }
-
- it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
- it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
- end
-
describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
- describe '#to_patch' do
- subject { commit.to_patch }
-
- it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
- it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
- end
-
describe '#to_hash' do
let(:hash) { commit.to_hash }
subject { hash }
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index c07b33a039a..6eb0600f49e 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
- let(:request_format) { :html }
+ let(:request) { double(:request, format: double(:format, ref: :html)) }
before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show', request_format: request_format)
+ controller = double(:controller, class: klass, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
@@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when the response content type is not :html' do
- let(:request_format) { :json }
+ let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 84ddbbbf2ee..8a52c151cc4 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -594,7 +594,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
is_expected.to have_html_escaped_body_text project.full_name
- is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 56b5d616284..5489c17bd82 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -5,7 +5,7 @@ describe Appearance do
it { is_expected.to be_valid }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
describe '.current', :use_clean_rails_memory_store_caching do
let!(:appearance) { create(:appearance) }
@@ -41,4 +41,12 @@ describe Appearance do
expect(new_row.valid?).to eq(false)
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', false do
+ let(:model_object) { create(:appearance, :with_logo) }
+ let(:upload_attribute) { :logo }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index e2b212f4f4c..0fbc934f669 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -626,62 +626,26 @@ describe Ci::Runner do
end
describe '.assignable_for' do
- let(:runner) { create(:ci_runner) }
+ let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) }
+ let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, runner_type: :group_type) }
+ let!(:instance_runner) { create(:ci_runner, :shared) }
let(:project) { create(:project) }
let(:another_project) { create(:project) }
- before do
- project.runners << runner
- end
-
- context 'with shared runners' do
- before do
- runner.update(is_shared: true)
- end
-
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
-
- context 'does not give shared runner' do
- subject { described_class.assignable_for(another_project) }
-
- it { is_expected.to be_empty }
- end
- end
-
- context 'with unlocked runner' do
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
+ context 'with already assigned project' do
+ subject { described_class.assignable_for(project) }
- context 'does give a specific runner' do
- subject { described_class.assignable_for(another_project) }
-
- it { is_expected.to contain_exactly(runner) }
- end
+ it { is_expected.to be_empty }
end
- context 'with locked runner' do
- before do
- runner.update(locked: true)
- end
-
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
-
- context 'does not give a locked runner' do
- subject { described_class.assignable_for(another_project) }
+ context 'with a different project' do
+ subject { described_class.assignable_for(another_project) }
- it { is_expected.to be_empty }
- end
+ it { is_expected.to include(unlocked_project_runner) }
+ it { is_expected.not_to include(group_runner) }
+ it { is_expected.not_to include(locked_project_runner) }
+ it { is_expected.not_to include(instance_runner) }
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 4e6b037a720..448b813c2e1 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -182,7 +182,6 @@ eos
it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) }
it { is_expected.to respond_to(:id) }
- it { is_expected.to respond_to(:to_patch) }
end
describe '#closes_issues' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2ed29052dc1..f3f2bc28d2c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
+
+ describe '#present' do
+ subject { commit_status.present }
+
+ it { is_expected.to be_a(CommitStatusPresenter) }
+ end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 673049d1cc4..a3e68d2e646 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
+
+ describe '#present' do
+ subject { generic_commit_status.present }
+
+ it { is_expected.to be_a(GenericCommitStatusPresenter) }
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0907d28d33b..f83b52e8975 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -15,7 +15,7 @@ describe Group do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
@@ -691,4 +691,12 @@ describe Group do
end
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', true do
+ let(:model_object) { create(:group, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5b452f17979..39625b559eb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -76,7 +76,7 @@ describe Project do
it { is_expected.to have_many(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:delete_all) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
@@ -3739,4 +3739,12 @@ describe Project do
it { is_expected.to be_nil }
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', true do
+ let(:model_object) { create(:project, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a7755a505d8..ac8d9a32d4e 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -990,65 +990,25 @@ describe Repository do
subject { repository.add_branch(user, branch_name, target) }
- context 'with Gitaly enabled' do
- it "calls Gitaly's OperationService" do
- expect_any_instance_of(Gitlab::GitalyClient::OperationService)
- .to receive(:user_create_branch).with(branch_name, user, target)
- .and_return(nil)
-
- subject
- end
-
- it 'creates_the_branch' do
- expect(subject.name).to eq(branch_name)
- expect(repository.find_branch(branch_name)).not_to be_nil
- end
-
- context 'with a non-existing target' do
- let(:target) { 'fake-target' }
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_create_branch).with(branch_name, user, target)
+ .and_return(nil)
- it "returns false and doesn't create the branch" do
- expect(subject).to be(false)
- expect(repository.find_branch(branch_name)).to be_nil
- end
- end
+ subject
end
- context 'with Gitaly disabled', :disable_gitaly do
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- hook = double(trigger: [true, nil])
- expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
-
- expect { subject }.not_to raise_error
- end
-
- it 'creates the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- expect(subject.name).to eq(branch_name)
- end
-
- it 'calls the after_create_branch hook' do
- expect(repository).to receive(:after_create_branch)
-
- subject
- end
- end
-
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- end
+ it 'creates_the_branch' do
+ expect(subject.name).to eq(branch_name)
+ expect(repository.find_branch(branch_name)).not_to be_nil
+ end
- it 'does not create the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+ context 'with a non-existing target' do
+ let(:target) { 'fake-target' }
- expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch(branch_name)).to be_nil
- end
+ it "returns false and doesn't create the branch" do
+ expect(subject).to be(false)
+ expect(repository.find_branch(branch_name)).to be_nil
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bb5308221f0..684fa030baf 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -39,7 +39,7 @@ describe User do
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
@@ -1223,6 +1223,24 @@ describe User do
end
end
+ describe '#accept_pending_invitations!' do
+ let(:user) { create(:user, email: 'user@email.com') }
+ let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
+ let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
+ let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') }
+ let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') }
+
+ it 'accepts all the user members pending invitations and returns the accepted_members' do
+ accepted_members = user.accept_pending_invitations!
+
+ expect(accepted_members).to match_array([project_member_invite, group_member_invite])
+ expect(group_member_invite.reload).not_to be_invite
+ expect(project_member_invite.reload).not_to be_invite
+ expect(external_project_member_invite.reload).to be_invite
+ expect(external_group_member_invite.reload).to be_invite
+ end
+ end
+
describe '#all_emails' do
let(:user) { create(:user) }
@@ -1786,28 +1804,54 @@ describe User do
end
end
- describe '#ci_authorized_runners' do
+ describe '#ci_owned_runners' do
let(:user) { create(:user) }
- let(:runner) { create(:ci_runner) }
+ let(:runner_1) { create(:ci_runner) }
+ let(:runner_2) { create(:ci_runner) }
- before do
- project.runners << runner
- end
-
- context 'without any projects' do
- let(:project) { create(:project) }
+ context 'without any projects nor groups' do
+ let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:group) { create(:group) }
it 'does not load' do
- expect(user.ci_authorized_runners).to be_empty
+ expect(user.ci_owned_runners).to be_empty
end
end
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let(:project) { create(:project, namespace: namespace) }
+ let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
it 'loads' do
- expect(user.ci_authorized_runners).to contain_exactly(runner)
+ expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ end
+ end
+
+ context 'with personal group runner' do
+ let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:group) do
+ create(:group, runners: [runner_2]).tap do |group|
+ group.add_owner(user)
+ end
+ end
+
+ it 'loads' do
+ expect(user.ci_owned_runners).to contain_exactly(runner_2)
+ end
+ end
+
+ context 'with personal project and group runner' do
+ let(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+
+ let!(:group) do
+ create(:group, runners: [runner_2]).tap do |group|
+ group.add_owner(user)
+ end
+ end
+
+ it 'loads' do
+ expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
end
end
@@ -1818,7 +1862,7 @@ describe User do
end
it 'loads' do
- expect(user.ci_authorized_runners).to contain_exactly(runner)
+ expect(user.ci_owned_runners).to contain_exactly(runner_1)
end
end
@@ -1828,14 +1872,28 @@ describe User do
end
it 'does not load' do
- expect(user.ci_authorized_runners).to be_empty
+ expect(user.ci_owned_runners).to be_empty
end
end
end
context 'with groups projects runners' do
let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ let!(:project) { create(:project, group: group, runners: [runner_1]) }
+
+ def add_user(access)
+ group.add_user(user, access)
+ end
+
+ it_behaves_like :member
+ end
+
+ context 'with groups runners' do
+ let!(:group) do
+ create(:group, runners: [runner_1]).tap do |group|
+ group.add_owner(user)
+ end
+ end
def add_user(access)
group.add_user(user, access)
@@ -1845,7 +1903,7 @@ describe User do
end
context 'with other projects runners' do
- let(:project) { create(:project) }
+ let!(:project) { create(:project, runners: [runner_1]) }
def add_user(access)
project.add_role(user, access)
@@ -1858,7 +1916,7 @@ describe User do
let(:group) { create(:group) }
let(:another_user) { create(:user) }
let(:subgroup) { create(:group, parent: group) }
- let(:project) { create(:project, group: subgroup) }
+ let!(:project) { create(:project, group: subgroup, runners: [runner_1]) }
def add_user(access)
group.add_user(user, access)
@@ -2769,4 +2827,12 @@ describe User do
expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts)
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', false do
+ let(:model_object) { create(:user, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 4bc005df2fc..efd175247b5 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
- expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
new file mode 100644
index 00000000000..f81ee44e371
--- /dev/null
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe CommitStatusPresenter do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:presenter) do
+ described_class.new(build)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 981ac768e3a..c7587c877fc 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -27,7 +27,7 @@ describe API::Runners do
end
end
- let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) }
before do
# Set project access for users
@@ -48,7 +48,7 @@ describe API::Runners do
expect(json_response).to be_an Array
expect(json_response[0]).to have_key('ip_address')
expect(descriptions).to contain_exactly(
- 'Project runner', 'Two projects runner'
+ 'Project runner', 'Two projects runner', 'Group runner'
)
expect(shared).to be_falsey
end
@@ -592,6 +592,15 @@ describe API::Runners do
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(201)
end
+
+ it 'enables a shared runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id
+ end.to change { project.runners.count }.by(1)
+
+ expect(shared_runner.reload).not_to be_shared
+ expect(response).to have_gitlab_http_status(201)
+ end
end
context 'user is not admin' do
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
new file mode 100644
index 00000000000..47ad0c6345d
--- /dev/null
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
+ describe '.destroy' do
+ before do
+ stub_uploads_object_storage(uploader_class)
+
+ model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ it 'deletes remote uploads' do
+ expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
+
+ expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ end
+
+ it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do
+ create(:upload, uploader: FileUploader, model: model_object)
+
+ expect { model_object.destroy }.to change { Upload.count }.by(-2)
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index f28bf430f02..98d4456b277 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do
expect(rendered).to have_text 'Registry'
end
- it 'highlights only one tab' do
+ it 'highlights sidebar item and flyout' do
render
- expect(rendered).to have_css('.active', count: 1)
+ expect(rendered).to have_css('.sidebar-top-level-items > li.active', count: 1)
+ expect(rendered).to have_css('.is-fly-out-only > li.active', count: 1)
end
- it 'highlights container registry tab only' do
+ it 'highlights container registry tab' do
render
- expect(rendered).to have_css('.active', text: 'Registry')
+ expect(rendered).to have_css('.sidebar-top-level-items > li.active', text: 'Registry')
end
end
end