Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock9
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/gl_dropdown.js14
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js25
-rw-r--r--app/assets/javascripts/layout_nav.js15
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue15
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph_row.vue41
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js26
-rw-r--r--app/assets/javascripts/new_sidebar.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar_height_manager.js37
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/new_nav.scss270
-rw-r--r--app/assets/stylesheets/new_sidebar.scss10
-rw-r--r--app/assets/stylesheets/pages/boards.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss26
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb23
-rw-r--r--app/helpers/page_layout_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/member.rb62
-rw-r--r--app/models/project.rb1
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/_snippets_head.html.haml6
-rw-r--r--app/views/dashboard/issues.html.haml11
-rw-r--r--app/views/dashboard/merge_requests.html.haml7
-rw-r--r--app/views/dashboard/milestones/index.html.haml7
-rw-r--r--app/views/groups/issues.html.haml4
-rw-r--r--app/views/groups/labels/index.html.haml4
-rw-r--r--app/views/groups/merge_requests.html.haml4
-rw-r--r--app/views/groups/milestones/index.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml26
-rw-r--r--app/views/layouts/admin.html.haml7
-rw-r--r--app/views/layouts/application.html.haml5
-rw-r--r--app/views/layouts/group.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml78
-rw-r--r--app/views/layouts/header/_new.html.haml84
-rw-r--r--app/views/layouts/header/_new_dropdown.haml8
-rw-r--r--app/views/layouts/nav/_admin.html.haml40
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml31
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml123
-rw-r--r--app/views/layouts/nav/_explore.html.haml30
-rw-r--r--app/views/layouts/nav/_group.html.haml31
-rw-r--r--app/views/layouts/nav/_new_dashboard.html.haml41
-rw-r--r--app/views/layouts/nav/_new_explore.html.haml19
-rw-r--r--app/views/layouts/nav/_profile.html.haml57
-rw-r--r--app/views/layouts/nav/_project.html.haml111
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml (renamed from app/views/layouts/nav/_new_admin_sidebar.html.haml)0
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml (renamed from app/views/layouts/nav/_new_group_sidebar.html.haml)0
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml (renamed from app/views/layouts/nav/_new_profile_sidebar.html.haml)0
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml (renamed from app/views/layouts/nav/_new_project_sidebar.html.haml)0
-rw-r--r--app/views/layouts/profile.html.haml7
-rw-r--r--app/views/layouts/project.html.haml7
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml5
-rw-r--r--app/views/projects/_flash_messages.html.haml3
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/issues/index.html.haml7
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/index.html.haml7
-rw-r--r--app/views/projects/milestones/index.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml3
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml15
-rw-r--r--app/views/projects/services/edit.html.haml4
-rw-r--r--app/views/projects/snippets/index.html.haml4
-rw-r--r--app/views/projects/tags/index.html.haml1
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/icons/_caret_down.svg1
-rw-r--r--app/views/shared/icons/_mr_bold.svg3
-rw-r--r--app/views/shared/icons/_plus_square.svg1
-rw-r--r--app/views/shared/icons/_todo_done.svg1
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml3
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml22
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml5
-rw-r--r--changelogs/unreleased/36860-migrate-issues-author.yml5
-rw-r--r--changelogs/unreleased/api-gpg-key-management.yml5
-rw-r--r--changelogs/unreleased/fuzzy-issue-search.yml5
-rw-r--r--changelogs/unreleased/sh-bump-jira-gem.yml5
-rw-r--r--db/migrate/20170825104051_migrate_issues_to_ghost_user.rb36
-rw-r--r--db/migrate/20170901071411_add_foreign_key_to_issue_author.rb14
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/README.md15
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/api/project_snippets.md3
-rw-r--r--doc/api/users.md224
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_system_notes.pngbin2330 -> 4214 bytes
-rw-r--r--doc/user/project/pipelines/settings.md28
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md10
-rw-r--r--doc/user/search/img/issue_search_by_term.pngbin0 -> 127492 bytes
-rw-r--r--doc/user/search/index.md14
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/users.rb150
-rw-r--r--lib/gitlab/git/repository.rb18
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb14
-rw-r--r--lib/gitlab/sql/pattern.rb23
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb28
-rw-r--r--lib/system_check/base_check.rb19
-rw-r--r--lib/system_check/incoming_email/foreman_configured_check.rb23
-rw-r--r--lib/system_check/incoming_email/imap_authentication_check.rb45
-rw-r--r--lib/system_check/incoming_email/initd_configured_check.rb32
-rw-r--r--lib/system_check/incoming_email/mail_room_running_check.rb43
-rw-r--r--lib/system_check/simple_executor.rb4
-rw-r--r--lib/tasks/gitlab/check.rake125
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb18
-rw-r--r--spec/features/issues/issue_detail_spec.rb14
-rw-r--r--spec/javascripts/gl_dropdown_spec.js323
-rw-r--r--spec/javascripts/monitoring/graph_row_spec.js62
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb38
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb120
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb24
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb51
-rw-r--r--spec/models/concerns/issuable_spec.rb42
-rw-r--r--spec/models/group_spec.rb19
-rw-r--r--spec/models/member_spec.rb9
-rw-r--r--spec/models/project_spec.rb19
-rw-r--r--spec/requests/api/users_spec.rb326
-rw-r--r--spec/support/group_members_shared_example.rb27
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb (renamed from spec/views/layouts/nav/_project.html.haml_spec.rb)6
138 files changed, 2086 insertions, 1451 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b08c441028b..778d33fb960 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -208,7 +208,7 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- - rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json
+ - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
flaky-examples-check:
<<: *dedicated-runner
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 93d4c1ef06f..0f1a7dfc7c4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.36.0
+0.37.0
diff --git a/Gemfile b/Gemfile
index 61c941ae449..0341f2609ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0'
gem 'hipchat', '~> 1.5.0'
# JIRA integration
-gem 'jira-ruby', '~> 1.1.2'
+gem 'jira-ruby', '~> 1.4'
# Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
@@ -397,7 +397,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index cba30e856ed..320d42b8974 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.31.0)
+ gitaly-proto (0.32.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -404,8 +404,9 @@ GEM
cause
json
ipaddress (0.8.3)
- jira-ruby (1.1.2)
+ jira-ruby (1.4.1)
activesupport
+ multipart-post
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
@@ -1020,7 +1021,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.31.0)
+ gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
@@ -1042,7 +1043,7 @@ DEPENDENCIES
html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
- jira-ruby (~> 1.1.2)
+ jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2)
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 1c5ca1d3cf9..23040cd9eb8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
- tag: `<${tokenKey.tag}>`,
+ tag: `:${tokenKey.tag}`,
type: tokenKey.type,
}));
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d65bbc0d808..6f7671aa6fe 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
- if (field.length) {
- selected = true;
+ if (value) {
+ value = value.toString().replace(/'/g, '\\\'');
+ field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ field = this.dropdown.parent().find(`input[name='${fieldName}']`);
+ selected = !field.length;
}
}
// Set URL
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index d314f3c4d43..0e8a0519928 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -5,7 +5,6 @@
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar {
new SubscriptionSelect();
}
- getNavHeight() {
- const navbarHeight = $('.navbar-gitlab').outerHeight();
- const layoutNavHeight = $('.layout-nav').outerHeight();
- const subNavScroll = $('.sub-nav-scroll').outerHeight();
- return navbarHeight + layoutNavHeight + subNavScroll;
- }
-
setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar {
this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable);
-
- if (enable) {
- this.initAffix();
- SidebarHeightManager.init();
- }
- }
-
- initAffix() {
- if (!this.$sidebar.hasClass('affix-top')) {
- const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight();
-
- this.$sidebar.affix({
- offset: {
- top: offsetTop,
- },
- });
- }
}
updateSelectedIssuableIds() {
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 5c1ba416a03..d064a2c0024 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav';
});
});
- function applyScrollNavClass() {
- const scrollOpacityHeight = 40;
- $('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
- }
-
$(() => {
- if (Cookies.get('new_nav') === 'true') {
- const newNavSidebar = new NewNavSidebar();
- newNavSidebar.bindEvents();
-
- initFlyOutNav();
- }
+ const newNavSidebar = new NewNavSidebar();
+ newNavSidebar.bindEvents();
- $(window).on('scroll', _.throttle(applyScrollNavClass, 100));
+ initFlyOutNav();
});
}).call(window);
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 74244faa5d9..b596c4f383f 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -4,7 +4,7 @@
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
- import GraphRow from './graph_row.vue';
+ import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
@@ -32,8 +32,8 @@
},
components: {
+ Graph,
GraphGroup,
- GraphRow,
EmptyState,
},
@@ -127,10 +127,10 @@
:key="index"
:name="groupData.group"
>
- <graph-row
- v-for="(row, index) in groupData.metrics"
+ <graph
+ v-for="(graphData, index) in groupData.metrics"
:key="index"
- :row-data="row"
+ :graph-data="graphData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 9c785f4ada8..cde2ff7ca2a 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -19,10 +19,6 @@
type: Object,
required: true,
},
- classType: {
- type: String,
- required: true,
- },
updateAspectRatio: {
type: Boolean,
required: true,
@@ -207,12 +203,11 @@
},
};
</script>
+
<template>
- <div
- :class="classType">
- <h5
- class="text-center graph-title">
- {{graphData.title}}
+ <div class="prometheus-graph">
+ <h5 class="text-center graph-title">
+ {{graphData.title}}
</h5>
<div
class="prometheus-svg-container"
@@ -243,7 +238,7 @@
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
- <monitoring-paths
+ <monitoring-paths
v-for="(path, index) in timeSeries"
:key="index"
:generated-line-path="path.linePath"
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 32c90fda8cc..958f537d31b 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -14,7 +14,7 @@ export default {
<div class="panel-heading">
<h4>{{name}}</h4>
</div>
- <div class="panel-body">
+ <div class="panel-body prometheus-graph-group">
<slot />
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/graph_row.vue b/app/assets/javascripts/monitoring/components/graph_row.vue
deleted file mode 100644
index bdb9149c3b4..00000000000
--- a/app/assets/javascripts/monitoring/components/graph_row.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
- import Graph from './graph.vue';
-
- export default {
- props: {
- rowData: {
- type: Array,
- required: true,
- },
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- },
- components: {
- Graph,
- },
- computed: {
- bootstrapClass() {
- return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
- },
- },
- };
-</script>
-
-<template>
- <div class="prometheus-row row">
- <graph
- v-for="(graphData, index) in rowData"
- :graph-data="graphData"
- :class-type="bootstrapClass"
- :key="index"
- :update-aspect-ratio="updateAspectRatio"
- :deployment-data="deploymentData"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 0a4cdd88044..7592af5878e 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -20,22 +20,6 @@ function normalizeMetrics(metrics) {
}));
}
-function collate(array, rows = 2) {
- const collatedArray = [];
- let row = [];
- array.forEach((value, index) => {
- row.push(value);
- if ((index + 1) % rows === 0) {
- collatedArray.push(row);
- row = [];
- }
- });
- if (row.length > 0) {
- collatedArray.push(row);
- }
- return collatedArray;
-}
-
export default class MonitoringStore {
constructor() {
this.groups = [];
@@ -45,7 +29,7 @@ export default class MonitoringStore {
storeMetrics(groups = []) {
this.groups = groups.map(group => ({
...group,
- metrics: collate(normalizeMetrics(sortMetrics(group.metrics))),
+ metrics: normalizeMetrics(sortMetrics(group.metrics)),
}));
}
@@ -54,12 +38,6 @@ export default class MonitoringStore {
}
getMetricsCount() {
- let metricsCount = 0;
- this.groups.forEach((group) => {
- group.metrics.forEach((metric) => {
- metricsCount = metricsCount += metric.length;
- });
- });
- return metricsCount;
+ return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
}
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index 05e3f33f5ed..709a5d33b9f 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -63,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') {
- const collapse = Cookies.get('sidebar_collapsed') === 'true';
+ const collapse = this.$sidebar.hasClass('sidebar-icons-only');
this.toggleCollapsedSidebar(collapse);
}
}
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 4c87d46c96e..a4eae135403 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,7 +2,6 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
-import SidebarHeightManager from './sidebar_height_manager';
(function() {
this.Sidebar = (function() {
@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager';
};
Sidebar.prototype.addEventListeners = function() {
- SidebarHeightManager.init();
const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js
deleted file mode 100644
index 2752fe2b911..00000000000
--- a/app/assets/javascripts/sidebar_height_manager.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import _ from 'underscore';
-import Cookies from 'js-cookie';
-
-export default {
- init() {
- if (!this.initialized) {
- if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return;
-
- this.$window = $(window);
- this.$rightSidebar = $('.js-right-sidebar');
- this.$navHeight = $('.navbar-gitlab').outerHeight() +
- $('.layout-nav').outerHeight() +
- $('.sub-nav-scroll').outerHeight();
-
- const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
- const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
-
- this.$window.on('scroll', throttledSetSidebarHeight);
- this.$window.on('resize', debouncedSetSidebarHeight);
- this.initialized = true;
- }
- },
-
- setSidebarHeight() {
- const currentScrollDepth = window.pageYOffset || 0;
- const diff = this.$navHeight - currentScrollDepth;
-
- if (diff > 0) {
- const newSidebarHeight = window.innerHeight - diff;
- this.$rightSidebar.outerHeight(newSidebarHeight);
- this.sidebarHeightIsCustom = true;
- } else if (this.sidebarHeightIsCustom) {
- this.$rightSidebar.outerHeight('100%');
- this.sidebarHeightIsCustom = false;
- }
- },
-};
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index dada7a274f3..5f397f08936 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -766,6 +766,7 @@
box-shadow: none;
padding: 8px 16px;
text-align: left;
+ white-space: normal;
width: 100%;
// make sure the text color is not overriden
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 35bd97980e2..b00a2d053e2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -105,12 +105,11 @@ header {
top: -3px;
font-size: 10px;
}
+ }
+ .user-counter {
svg {
- position: relative;
- top: 2px;
- height: 17px;
- // hack to get SVG to line up with FA icons
+ height: 16px;
width: 23px;
fill: currentColor;
}
@@ -325,12 +324,12 @@ header {
li {
.badge {
position: inherit;
- top: -8px;
font-weight: $gl-font-weight-normal;
- margin-left: -11px;
+ margin-left: -6px;
font-size: 11px;
color: $white-light;
- padding: 1px 5px 2px;
+ padding: 0 5px;
+ line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 01fffa717e9..88b08998dfd 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -177,13 +177,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
+$new-navbar-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
-$border-radius-default: 3px;
+$border-radius-default: 4px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 3ea67c76503..e4b52ab480d 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -3,15 +3,21 @@
@import "bootstrap/variables";
@import "framework/mixins";
+.content-wrapper.page-with-new-nav {
+ margin-top: $new-navbar-height;
+}
+
header.navbar-gitlab-new {
color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
+ min-height: $new-navbar-height;
.header-content {
display: -webkit-flex;
display: flex;
padding-left: 0;
+ min-height: $new-navbar-height;
.title-container {
display: -webkit-flex;
@@ -39,20 +45,13 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
align-items: center;
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- margin-left: -$gl-padding;
-
- @media (min-width: $screen-sm-min) {
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- }
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: $border-radius-default;
svg {
- margin-top: -3px;
-
@media (min-width: $screen-sm-min) {
- margin-right: 10px;
+ margin-right: 8px;
}
}
@@ -61,7 +60,7 @@ header.navbar-gitlab-new {
svg {
width: 55px;
- height: 15px;
+ height: 14px;
margin: 0;
fill: $white-light;
}
@@ -69,9 +68,7 @@ header.navbar-gitlab-new {
&:hover,
&:focus {
- .logo-text svg {
- fill: $tanuki-yellow;
- }
+ background-color: rgba($indigo-200, .2);
}
}
}
@@ -91,6 +88,20 @@ header.navbar-gitlab-new {
right: 0;
}
}
+
+ &.menu-expanded {
+ @media (max-width: $screen-xs-max) {
+ .title-container,
+ .header-logo, {
+ display: none;
+ }
+ }
+ }
+ }
+
+ .dropdown-bold-header {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
}
.navbar-collapse {
@@ -99,14 +110,10 @@ header.navbar-gitlab-new {
box-shadow: 0;
@media (max-width: $screen-xs-max) {
- margin-left: -$gl-padding;
+ margin-left: -8px;
margin-right: -10px;
}
- .dropdown-bold-header {
- color: initial;
- }
-
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
@@ -120,7 +127,7 @@ header.navbar-gitlab-new {
.container-fluid {
.navbar-toggle {
min-width: 45px;
- padding: 6px $gl-padding;
+ padding: 4px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
@@ -157,31 +164,90 @@ header.navbar-gitlab-new {
}
> a {
- background: none;
will-change: color;
+ margin: 4px 2px;
+ padding: 6px 8px;
+ color: $indigo-200;
+ height: 32px;
+
+ @media (max-width: $screen-xs-max) {
+ padding: 0;
+ }
+
+ svg {
+ fill: $indigo-200;
+ }
&.header-user-dropdown-toggle {
+ margin-left: 2px;
+
.header-user-avatar {
border-color: $indigo-200;
+ margin-right: 0;
}
}
+ }
- &:hover,
- &:focus {
- color: $white-light;
- opacity: 1;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
- > svg {
- fill: $white-light;
- }
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
+
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($indigo-200, .2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
}
}
}
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ margin-right: 1px;
+ background-color: $white-light;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ svg {
+ fill: $indigo-900;
+ }
+ }
+
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ i {
+ color: $orange-500;
+ font-size: 20px;
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+ }
}
}
}
@@ -189,45 +255,76 @@ header.navbar-gitlab-new {
.navbar-sub-nav {
display: -webkit-flex;
display: flex;
- margin-bottom: 0;
+ margin: 0 0 0 6px;
color: $indigo-200;
- > li {
- > a:hover,
- > a:focus {
- box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
+ .dropdown-chevron {
+ position: relative;
+ top: -1px;
+ font-size: 10px;
+ }
+}
- &.active > a {
- box-shadow: inset 0 -3px 0 $indigo-500;
- color: $white-light;
- font-weight: $gl-font-weight-bold;
- }
+.navbar-gitlab-new {
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ background-color: rgba($indigo-200, .2);
- > a {
- display: block;
- padding: 16px 10px;
- font-size: 13px;
- color: currentColor;
- box-shadow: inset 0 0 0 transparent;
- will-change: box-shadow;
- transition: box-shadow 0.15s;
+ svg {
+ fill: currentColor;
+ }
+ }
- @media (min-width: $screen-sm-min) {
- padding: 15px $gl-padding;
- font-size: 14px;
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ > a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 8px;
+ margin: 4px 2px;
+ font-size: 12px;
+ color: currentColor;
+ border-radius: $border-radius-default;
+ height: 32px;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($indigo-200, .2);
+ margin: 8px;
}
}
}
+}
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
+.admin-icon i {
+ font-size: 18px;
+}
+
+.caret-down {
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
}
.header-user .dropdown-menu-nav,
@@ -236,10 +333,14 @@ header.navbar-gitlab-new {
}
.search {
+ margin: 4px 8px 0;
+
form {
+ height: 32px;
border: 0;
+ border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s;
+ transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
background-color: rgba($indigo-200, .3);
@@ -248,31 +349,50 @@ header.navbar-gitlab-new {
}
&.search-active form {
- background-color: rgba($indigo-200, .3);
+ background-color: $white-light;
box-shadow: none;
+
+ .search-input {
+ color: $gl-text-color;
+ transition: color ease-in-out 0.15s;
+ }
+
+ .search-input::placeholder {
+ color: $gl-text-color-tertiary;
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: $gl-text-color-tertiary;
+ transition: color ease-in-out 0.15s;
+ }
+ }
}
.search-input {
color: $white-light;
background: none;
+ transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: rgba($indigo-200, .8);
+ transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
- transition: color 0.15s;
will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
- height: 34px;
+ height: 32px;
+ transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
@@ -284,8 +404,9 @@ header.navbar-gitlab-new {
&.search-active {
.location-badge {
- color: $white-light;
- background-color: rgba($indigo-200, .2);
+ color: $gl-text-color;
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
}
.search-input-wrap {
@@ -403,3 +524,14 @@ header.navbar-gitlab-new {
}
}
}
+
+.btn-sign-in {
+ margin-top: 3px;
+ background-color: $indigo-100;
+ color: $indigo-900;
+ font-weight: $gl-font-weight-bold;
+
+ &:hover {
+ background-color: $white-light;
+ }
+}
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index e21d52b7db6..55e0343d0dc 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute
.right-sidebar {
position: fixed;
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -92,7 +92,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400;
width: $new-sidebar-width;
transition: left $sidebar-transition-duration;
- top: $header-height;
+ top: $new-navbar-height;
bottom: 0;
left: 0;
background-color: $gray-normal;
@@ -188,7 +188,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .nav-sidebar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
.sidebar-sub-level-items {
@@ -452,7 +452,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone
.boards-list {
- height: calc(100vh - #{$header-height});
+ height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
@@ -463,7 +463,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .boards-list {
- height: calc(100vh - #{$header-height} - #{$performance-bar-height});
+ height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 0f3074076ce..314dd2d1a21 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -440,6 +440,7 @@
&.right-sidebar {
top: 0;
bottom: 0;
+ height: 100%;
}
.issuable-sidebar-header {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index a52ac0d53e7..9362d80d4e6 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -227,6 +227,26 @@
margin-top: 20px;
}
+.prometheus-graph-group {
+ display: flex;
+ flex-wrap: wrap;
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph {
+ flex: 1 0 auto;
+ min-width: 450px;
+ padding: $gl-padding / 2;
+
+ h5 {
+ font-size: 16px;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ min-width: 100%;
+ }
+}
+
.prometheus-svg-container {
position: relative;
height: 0;
@@ -297,9 +317,3 @@
}
}
}
-
-.prometheus-row {
- h5 {
- font-size: 16px;
- }
-}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 8d73246223d..615020ca856 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
}
.search-holder {
+ @include new-style-dropdown;
+
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 017df8f6794..8d02d5de5c3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -302,10 +302,6 @@ module ApplicationHelper
end
end
- def show_new_nav?
- true
- end
-
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index d330bd644e5..9e7c25492b7 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -86,7 +86,7 @@ module GroupsHelper
def group_title_link(group, hidable: false, show_avatar: false)
link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do
output =
- if (show_new_nav? && group.try(:avatar_url) || (show_new_nav? && show_avatar)) && !Rails.env.test?
+ if (group.try(:avatar_url) || show_avatar)) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
else
""
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index b63b3b70903..a23a43c9f43 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,8 +1,8 @@
module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
- class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar
- class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar
+ class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar
+ class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name
end
@@ -30,24 +30,15 @@ module NavHelper
end
end
- def nav_header_class
- class_names = []
- class_names << 'with-horizontal-nav' if defined?(nav) && nav
-
- class_names
+ def nav_control_class
+ "nav-control" if current_user
end
- def layout_nav_class
- return [] if show_new_nav?
-
+ def user_dropdown_class
class_names = []
- class_names << 'page-with-layout-nav' if defined?(nav) && nav
- class_names << 'page-with-sub-nav' if content_for?(:sub_nav)
+ class_names << 'header-user-dropdown-toggle'
+ class_names << 'impersonated-user' if session[:impersonator_id]
class_names
end
-
- def nav_control_class
- "nav-control" if current_user
- end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index d2b2bd4b451..38a66ae1de9 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -4,7 +4,7 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any?
- if show_new_nav? && titles.any? && !defined?(@breadcrumb_title)
+ if titles.any? && !defined?(@breadcrumb_title)
@breadcrumb_title = @page_title.last
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 9b6acedb40f..b07cb7a775a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -62,7 +62,7 @@ module ProjectsHelper
project_link = link_to project_path(project), { class: ("project-item-select-holder" unless show_new_nav?) } do
output =
- if show_new_nav? && project.avatar_url && !Rails.env.test?
+ if project.avatar_url && !Rails.env.test?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
else
""
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3731b7c8577..681c3241dbb 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include Gitlab::SQL::Pattern
include CacheMarkdownField
include Participable
include Mentionable
@@ -122,7 +123,9 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
- where(arel_table[:title].matches("%#{query}%"))
+ title = to_fuzzy_arel(:title, query)
+
+ where(title)
end
# Searches for records with a matching title or description.
@@ -133,10 +136,10 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
- t = arel_table
- pattern = "%#{query}%"
+ title = to_fuzzy_arel(:title, query)
+ description = to_fuzzy_arel(:description, query)
- where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
+ where(title&.or(description))
end
def sort(method, excluded_labels: [])
diff --git a/app/models/group.rb b/app/models/group.rb
index 190b27cf66b..e746e4a12c9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -16,6 +16,7 @@ class Group < Namespace
source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+ has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/member.rb b/app/models/member.rb
index ee2cb13697b..cbbd58f2eaf 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- def add_user(source, user, access_level, current_user: nil, expires_at: nil)
- user = retrieve_user(user)
+ def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
+ # `user` can be either a User object, User ID or an email to be invited
+ member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
- # `user` can be either a User object or an email to be invited
- member =
- if user.is_a?(User)
- source.members.find_by(user_id: user.id) ||
- source.requesters.find_by(user_id: user.id) ||
- source.members.build(user_id: user.id)
- else
- source.members.build(invite_email: user)
- end
-
return member unless can_update_member?(current_user, member)
member.attributes = {
@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
- # Collect all user ids into separate array
- # so we can use single sql query to get user objects
- user_ids = users.select { |user| user =~ /\A\d+\Z/ }
- users = users - user_ids + User.where(id: user_ids)
+ emails, users, existing_members = parse_users_list(source, users)
self.transaction do
- users.map do |user|
+ (emails + users).map! do |user|
add_user(
source,
user,
access_level,
+ existing_members: existing_members,
current_user: current_user,
expires_at: expires_at
)
@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base
private
+ def parse_users_list(source, list)
+ emails, user_ids, users = [], [], []
+ existing_members = {}
+
+ list.each do |item|
+ case item
+ when User
+ users << item
+ when Integer
+ user_ids << item
+ when /\A\d+\Z/
+ user_ids << item.to_i
+ when Devise.email_regexp
+ emails << item
+ end
+ end
+
+ if user_ids.present?
+ users.concat(User.where(id: user_ids))
+ existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
+ end
+
+ [emails, users, existing_members]
+ end
+
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base
User.find_by(id: user) || User.find_by(email: user) || user
end
+ def retrieve_member(source, user, existing_members)
+ user = retrieve_user(user)
+
+ if user.is_a?(User)
+ if existing_members
+ existing_members[user.id] || source.members.build(user_id: user.id)
+ else
+ source.members_and_requesters.find_or_initialize_by(user_id: user.id)
+ end
+ else
+ source.members.build(invite_email: user)
+ end
+ end
+
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 051c4c8e2ec..3d89dabd96f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -144,6 +144,7 @@ class Project < ActiveRecord::Base
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 5a379eae8f4..11bf3f5d323 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,4 +1,4 @@
-- if show_new_nav? && current_user.can_create_group?
+- if current_user.can_create_group?
- content_for :breadcrumbs_extra do
= link_to "New group", new_group_path, class: "btn btn-new"
@@ -10,8 +10,8 @@
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore public groups' do
Explore public groups
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls.nav-controls-new-nav
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
- if current_user.can_create_group?
- = link_to "New group", new_group_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}"
+ = link_to "New group", new_group_path, class: "btn btn-new visible-xs"
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 1f9a5b401b6..e2a1914ada2 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,7 +1,7 @@
= content_for :flash_message do
= render 'shared/project_limit'
-- if show_new_nav? && current_user.can_create_project?
+- if current_user.can_create_project?
- content_for :breadcrumbs_extra do
= link_to "New project", new_project_path, class: 'btn btn-new'
@@ -19,8 +19,8 @@
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore projects
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls.nav-controls-new-nav
= render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
- = link_to "New project", new_project_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}"
+ = link_to "New project", new_project_path, class: "btn btn-new visible-xs"
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index fd5389106bb..14c18678ab1 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,4 +1,4 @@
-- if show_new_nav? && current_user
+- if current_user
- content_for :breadcrumbs_extra do
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
@@ -10,7 +10,3 @@
= nav_link(page: explore_snippets_path) do
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets
-
- - if current_user
- .nav-controls.hidden-xs{ class: ("hidden-sm hidden-md hidden-lg" if show_new_nav?) }
- = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 9ac44674b73..ad0e205a79f 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,15 +4,14 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do
- = icon('rss')
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
+- content_for :breadcrumbs_extra do
+ = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do
+ = icon('rss')
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss')
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 960e1e55f36..ccc74f7cf3d 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,13 +2,12 @@
- page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
+- content_for :breadcrumbs_extra do
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index cb8bf57cba1..9fffdded1a0 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -2,14 +2,13 @@
- page_title 'Milestones'
- header_title 'Milestones', dashboard_milestones_path
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
+- content_for :breadcrumbs_extra do
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
.milestones
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 837ef385dd5..13a4b4c90c9 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,7 +8,7 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
-- if show_new_nav? && group_issues_exists
+- if group_issues_exists
- content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
= icon('rss')
@@ -19,7 +19,7 @@
- if group_issues_exists
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= link_to params.merge(rss_url_options), class: 'btn' do
= icon('rss')
%span.icon-label
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 50179a47797..9e59a09d459 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,5 +1,5 @@
- page_title 'Labels'
-- if show_new_nav? && can?(current_user, :admin_label, @group)
+- if can?(current_user, :admin_label, @group)
- content_for :breadcrumbs_extra do
= link_to "New label", new_group_label_path(@group), class: "btn btn-new"
@@ -10,7 +10,7 @@
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
- if can?(current_user, :admin_label, @group)
= link_to "New label", new_group_label_path(@group), class: "btn btn-new"
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 184df6f5406..0344770e0dd 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -4,7 +4,7 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
-- if show_new_nav? && current_user
+- if current_user
- content_for :breadcrumbs_extra do
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
@@ -14,7 +14,7 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 66c6cc9e279..6e7a1af243d 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Milestones"
-- if show_new_nav? && can?(current_user, :admin_milestones, @group)
+- if can?(current_user, :admin_milestones, @group)
- content_for :breadcrumbs_extra do
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
@@ -8,7 +8,7 @@
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
- if can?(current_user, :admin_milestones, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 3babdae3968..34e85fef6d9 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -32,9 +32,9 @@
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- - if show_new_nav?
- = stylesheet_link_tag "new_nav", media: "all"
- = stylesheet_link_tag "new_sidebar", media: "all"
+ // TODO: Combine these 2 stylesheets into application.scss
+ = stylesheet_link_tag "new_nav", media: "all"
+ = stylesheet_link_tag "new_sidebar", media: "all"
= Gon::Base.render_data
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index c4f8cd71395..1fd301d6850 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,26 +1,14 @@
.page-with-sidebar{ class: page_with_sidebar_class }
- - if show_new_nav?
- - if defined?(nav) && nav
- = render "layouts/nav/#{nav}"
- - else
- - if defined?(nav) && nav
- .layout-nav
- .container-fluid
- = render "layouts/nav/#{nav}"
- - if content_for?(:sub_nav)
- = yield :sub_nav
- .content-wrapper{ class: layout_nav_class }
- - if show_new_nav?
- .mobile-overlay
+ - if defined?(nav) && nav
+ = render "layouts/nav/sidebar/#{nav}"
+ .content-wrapper.page-with-new-nav
+ .mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
- - if show_new_nav?
- - if content_for?(:new_global_flash)
- = yield :new_global_flash
- - unless @hide_breadcrumbs
- = render "layouts/nav/breadcrumbs"
- = render "layouts/flash"
= yield :flash_message
+ - unless @hide_breadcrumbs
+ = render "layouts/nav/breadcrumbs"
+ = render "layouts/flash"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index ae9eee215e0..8595157a997 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,9 +1,6 @@
- page_title "Admin Area"
- header_title "Admin Area", admin_root_path
-- if show_new_nav?
- - nav "new_admin_sidebar"
- - @new_sidebar = true
-- else
- - nav "admin"
+- nav "admin"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index b53f382fa3d..65ac8aaa59b 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -4,10 +4,7 @@
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- - if show_new_nav?
- = render "layouts/header/new"
- - else
- = render "layouts/header/default", title: header_title
+ = render "layouts/header/default"
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 35abfa0e80c..08bd6fc311e 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,10 +1,7 @@
- page_title @group.name
- page_description @group.description unless page_description
- header_title group_title(@group) unless header_title
-- if show_new_nav?
- - nav "new_group_sidebar"
- - @new_sidebar = true
-- else
- - nav "group"
+- nav "group"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 1d875f81041..d8fc371497d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,68 +1,50 @@
-%header.navbar.navbar-gitlab{ class: nav_header_class }
- .navbar-border
+%header.navbar.navbar-gitlab.navbar-gitlab-new
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
- .dropdown.global-dropdown
- %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.sr-only Toggle navigation
- = icon('bars')
- .dropdown-menu-nav.global-dropdown-menu
- - if current_user
- = render 'layouts/nav/dashboard'
- - else
- = render 'layouts/nav/explore'
+ .title-container
+ %h1.title
+ = link_to root_path, title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+ %span.logo-text.hidden-xs
+ = render 'shared/logo_type.svg'
- .header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
- = brand_header_logo
-
- .title-container.js-title-container
- %h1.title{ class: ('initializing' if @has_group_title) }= title
+ - if current_user
+ = render "layouts/nav/dashboard"
+ - else
+ = render "layouts/nav/explore"
.navbar-collapse.collapse
%ul.nav.navbar-nav
+ - if current_user
+ = render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- if current_user
- - if session[:impersonator_id]
- %li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.admin?
- %li
- = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- = render 'layouts/header/new_dropdown'
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %li.user-counter
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- %li
- = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %li.user-counter
+ = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- %li
+ %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
+ = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
- = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
- = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- = icon('caret-down')
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
@@ -74,18 +56,24 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
+ - if current_user
+ %li
+ = link_to "Help", help_path
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret')
- else
%li
%div
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+ = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
- %button.navbar-toggle{ type: 'button' }
+ %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
- = icon('ellipsis-v')
-
- = yield :header_content
+ = icon('ellipsis-v', class: 'js-navbar-toggle-right')
+ = icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
deleted file mode 100644
index c84d7053cd6..00000000000
--- a/app/views/layouts/header/_new.html.haml
+++ /dev/null
@@ -1,84 +0,0 @@
-%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class }
- %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
- .container-fluid
- .header-content
- .title-container
- %h1.title
- = link_to root_path, title: 'Dashboard', id: 'logo' do
- = brand_header_logo
- %span.logo-text.hidden-xs
- = render 'shared/logo_type.svg'
-
- - if current_user
- = render "layouts/nav/new_dashboard"
- - else
- = render "layouts/nav/new_explore"
-
- .navbar-collapse.collapse
- %ul.nav.navbar-nav
- %li.hidden-sm.hidden-xs
- = render 'layouts/search' unless current_controller?(:search)
- %li.visible-sm-inline-block.visible-xs-inline-block
- = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('search')
- - if current_user
- - if session[:impersonator_id]
- %li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.admin?
- %li
- = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- = render 'layouts/header/new_dropdown'
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('issues')
- - issues_count = assigned_issuables_count(:issues)
- %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
- = number_with_delimiter(issues_count)
- %li
- = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('mr_bold')
- - merge_requests_count = assigned_issuables_count(:merge_requests)
- %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
- = number_with_delimiter(merge_requests_count)
- %li
- = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
- %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
- = todos_count_format(todos_pending_count)
- %li.header-user.dropdown
- = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
- = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- = icon('chevron-down')
- .dropdown-menu-nav.dropdown-menu-align-right
- %ul
- %li.current-user
- .user-name.bold
- = current_user.name
- @#{current_user.username}
- %li.divider
- %li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
- %li
- = link_to "Settings", profile_path
- %li.divider
- %li
- = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
- - else
- %li
- %div
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
- %span.sr-only Toggle navigation
- = icon('ellipsis-v', class: 'js-navbar-toggle-right')
- = icon('times', class: 'js-navbar-toggle-left')
-
-= render 'shared/outdated_browser'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 9da739b0974..63d1c077ecd 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,11 +1,7 @@
%li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
- - if show_new_nav?
- = icon('plus')
- = icon('chevron-down')
- - else
- = icon('plus fw')
- = icon('caret-down')
+ = custom_icon('plus_square')
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
deleted file mode 100644
index 6df0adfd742..00000000000
--- a/app/views/layouts/nav/_admin.html.haml
+++ /dev/null
@@ -1,40 +0,0 @@
-= render 'layouts/nav/admin_settings'
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
- %span
- Overview
- = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
- = link_to admin_conversational_development_index_path, title: 'Monitoring' do
- %span
- Monitoring
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
- %span
- Messages
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to admin_hooks_path, title: 'Hooks' do
- %span
- System Hooks
-
- = nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications' do
- %span
- Applications
-
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse Reports" do
- %span
- Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
-
- - if akismet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
- %span
- Spam Logs
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
deleted file mode 100644
index 9de0e12a826..00000000000
--- a/app/views/layouts/nav/_admin_settings.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-.controls
- .dropdown.admin-settings-dropdown
- %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- %span
- Deploy Keys
-
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- %span
- Service Templates
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
- %span
- Labels
-
- = nav_link(controller: :appearances) do
- = link_to admin_appearances_path, title: 'Appearances' do
- %span
- Appearance
-
- %li.divider
- = nav_link(controller: :application_settings) do
- = link_to admin_application_settings_path, title: 'Settings' do
- %span
- Settings
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 3538646359e..feffd7707dc 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -2,8 +2,8 @@
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
- .breadcrumbs-container
- - if defined?(@new_sidebar)
+ .breadcrumbs-container{ class: [container, @content_class] }
+ - if defined?(@left_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar
= icon ('bars')
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index be7d27df2a0..8a39c4d775f 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,67 +1,62 @@
-%ul
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- P
- %span
- Projects
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- A
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
- %span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+%ul.list-unstyled.navbar-sub-nav
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
+ %a{ href: "#", data: { toggle: "dropdown" } }
+ Projects
+ = custom_icon('caret_down')
+ .dropdown-menu.projects-dropdown-menu
+ = render "layouts/nav/projects_dropdown/show"
+
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- G
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
+ Groups
+
+ = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ Activity
+
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- L
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- I
- %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues))
- %span
- Issues
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- M
- %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests))
- %span
- Merge Requests
- = nav_link(controller: 'dashboard/snippets') do
+ Milestones
+
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- S
- %span
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
+ Snippets
+
+ %li.dropdown.hidden-lg
+ %a{ href: "#", data: { toggle: "dropdown" } }
+ More
+ = custom_icon('caret_down')
+ .dropdown-menu
+ %ul
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
+ = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
+ Groups
+
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, title: 'Activity' do
+ Activity
+
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
+ Milestones
+
+ = nav_link(controller: 'dashboard/snippets') do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
+ Snippets
+
+ -# Shortcut to Dashboard > Projects
+ %li.hidden
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ Projects
+
+ - if current_user.admin? || Gitlab::Sherlock.enabled?
+ %li.line-separator.hidden-xs
+ - if current_user.admin?
+ = nav_link(controller: 'admin/dashboard') do
+ = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('wrench fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 0cb367452f7..cd1c39f3226 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,30 +1,12 @@
-%ul
+%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- P
- %span
- Projects
+ Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- G
- %span
- Groups
+ Groups
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- S
- %span
- Snippets
- %li.divider
- = nav_link(controller: :help) do
- = link_to help_path, title: 'Help' do
- %span
- Help
+ Snippets
+ %li
+ = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
deleted file mode 100644
index 261445ecd2b..00000000000
--- a/app/views/layouts/nav/_group.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'Home' do
- %span
- Group
- = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = link_to issues_group_path(@group), title: 'Issues' do
- %span
- Issues
- - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- %span.badge.count= number_with_delimiter(issues.count)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- %span
- Merge Requests
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- %span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group), title: 'Members' do
- %span
- Members
- - if current_user && can?(current_user, :admin_group, @group)
- = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = link_to edit_group_path(@group), title: 'Settings' do
- %span
- Settings
diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml
deleted file mode 100644
index e670e04928c..00000000000
--- a/app/views/layouts/nav/_new_dashboard.html.haml
+++ /dev/null
@@ -1,41 +0,0 @@
-%ul.list-unstyled.navbar-sub-nav
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
- %a{ href: '#', title: 'Projects', data: { toggle: 'dropdown' } }
- Projects
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu.projects-dropdown-menu
- = render "layouts/nav/projects_dropdown/show"
-
- = nav_link(controller: ['dashboard/groups', 'explore/groups']) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- Groups
-
- = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm hidden-md" }) do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- Activity
-
- %li.dropdown
- %a{ href: "#", data: { toggle: "dropdown" } }
- More
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu
- %ul
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm visible-md" }) do
- = link_to activity_dashboard_path, title: 'Activity' do
- Activity
-
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
- Milestones
-
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
-
- -# Shortcut to Dashboard > Projects
- %li.hidden
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- Projects
diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml
deleted file mode 100644
index 40385f251e3..00000000000
--- a/app/views/layouts/nav/_new_explore.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-%ul.list-unstyled.navbar-sub-nav
- = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- Projects
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
- Groups
- %li.dropdown
- %a{ href: "#", data: { toggle: "dropdown" } }
- More
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu
- %ul
- = nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
deleted file mode 100644
index 448f6abedf2..00000000000
--- a/app/views/layouts/nav/_profile.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-.scrolling-tabs-container
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile Settings' do
- %span
- Profile
- = nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account' do
- %span
- Account
- - if current_application_settings.user_oauth_applications?
- = nav_link(controller: 'oauth/applications') do
- = link_to applications_profile_path, title: 'Applications' do
- %span
- Applications
- = nav_link(controller: :chat_names) do
- = link_to profile_chat_names_path, title: 'Chat' do
- %span
- Chat
- = nav_link(controller: :personal_access_tokens) do
- = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
- %span
- Access Tokens
- = nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails' do
- %span
- Emails
- - unless current_user.ldap_user?
- = nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password' do
- %span
- Password
- = nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications' do
- %span
- Notifications
-
- = nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys' do
- %span
- SSH Keys
- = nav_link(controller: :gpg_keys) do
- = link_to profile_gpg_keys_path, title: 'GPG Keys' do
- %span
- GPG Keys
- = nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences' do
- %span
- Preferences
- = nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Authentication log' do
- %span
- Authentication log
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
deleted file mode 100644
index b88465848e3..00000000000
--- a/app/views/layouts/nav/_project.html.haml
+++ /dev/null
@@ -1,111 +0,0 @@
-- can_edit = can?(current_user, :admin_project, @project)
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- %span
- Project
-
- - if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
- = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
- %span
- Repository
-
- - if project_nav_tab? :container_registry
- = nav_link(controller: %w[projects/registry/repositories]) do
- = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
- %span
- Registry
-
- - if project_nav_tab? :issues
- = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
- %span
- Issues
- - if @project.issues_enabled?
- %span.badge.count.issue_counter
- = number_with_delimiter(@project.open_issues_count)
-
- - if project_nav_tab? :merge_requests
- - controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled?
- = nav_link(controller: controllers) do
- = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- %span
- Merge Requests
- %span.badge.count.merge_counter.js-merge-counter
- = number_with_delimiter(@project.open_merge_requests_count)
-
- - if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
-
- - if project_nav_tab? :wiki
- = nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
- %span
- Wiki
-
- - if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
- %span
- Snippets
-
- - if project_nav_tab? :project_members
- = nav_link(controller: :project_members) do
- = link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do
- %span
- Members
-
- - if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
- = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
- %span
- Settings
-
- -# Shortcut to Project > Activity
- %li.hidden
- = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- %span
- Activity
-
- -# Shortcut to Repository > Graph (formerly, Network)
- - if project_nav_tab? :network
- %li.hidden
- = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do
- Graph
-
- -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- - unless @project.empty_repo?
- %li.hidden
- = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
- Charts
-
- -# Shortcut to Issues > New Issue
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- Create a new issue
-
- -# Shortcut to Pipelines > Jobs
- - if project_nav_tab? :builds
- %li.hidden
- = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
- Jobs
-
- -# Shortcut to commits page
- - if project_nav_tab? :commits
- %li.hidden
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
- Commits
-
- -# Shortcut to issue boards
- %li.hidden
- = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 3b53117deb6..3b53117deb6 100644
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 5a1511b262f..5a1511b262f 100644
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index ccb6d1492f1..ccb6d1492f1 100644
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 760c4c97c33..760c4c97c33 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index c365839e605..67aa05b655c 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,10 +1,7 @@
- page_title "User Settings"
- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
-- if show_new_nav?
- - nav "new_profile_sidebar"
- - @new_sidebar = true
-- else
- - nav "profile"
+- nav "profile"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index d6db85ee87a..6b847fb4b7c 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,11 +1,8 @@
- page_title @project.name_with_namespace
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
-- if show_new_nav?
- - nav "new_project_sidebar"
- - @new_sidebar = true
-- else
- - nav "project"
+- nav "project"
+- @left_sidebar = true
- content_for :project_javascripts do
- project = @target_project || @project
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 33e062c1c9c..0b03276efcc 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -1,8 +1,5 @@
- page_title 'Two-Factor Authentication', 'Account'
-- if show_new_nav?
- - add_to_breadcrumbs("Account", profile_account_path)
-- else
- - header_title "Two-Factor Authentication", profile_two_factor_auth_path
+- add_to_breadcrumbs("Account", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index f47d84ef755..0175b519867 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -1,7 +1,6 @@
- project = local_assigns.fetch(:project)
-- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
-= content_for flash_message_container do
+= content_for :flash_message do
= render partial: 'deletion_failed', locals: { project: project }
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 30cdbc5ae04..acc80b49dd0 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
- page_title "Environments"
+- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index aacb057840d..6fcb5975707 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -13,15 +13,14 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render "projects/issues/nav_btns"
+- content_for :breadcrumbs_extra do
+ = render "projects/issues/nav_btns"
- if project_issues(@project).exists?
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index cc59a129aae..fbaf88356bf 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -39,8 +39,7 @@
%ul
- if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue)
- / TODO: simplify condition back #36860
- - if @issue.author && current_user != @issue.author
+ - unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 4b9da02c6b8..ec9e8444ac5 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,7 +3,7 @@
- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
-- if show_new_nav? && can?(current_user, :admin_label, @project)
+- if can?(current_user, :admin_label, @project)
- content_for :breadcrumbs_extra do
= link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new"
@@ -18,7 +18,7 @@
Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- if can_admin_label
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= link_to new_project_label_path(@project), class: "btn btn-new" do
New label
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index c020e7db380..27c3002366b 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -12,9 +12,8 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
+- content_for :breadcrumbs_extra do
+ = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'projects/last_push'
@@ -22,7 +21,7 @@
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index e0b29b0c2e1..71ec88ef1c1 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
- page_title 'Milestones'
-- if show_new_nav? && can?(current_user, :admin_milestone, @project)
+- if can?(current_user, :admin_milestone, @project)
- content_for :breadcrumbs_extra do
= link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
@@ -11,10 +11,10 @@
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls.nav-controls-new-nav
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-new visible-xs", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 69178aea32c..d9957b54a4d 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -7,7 +7,7 @@
- @no_container = true
- page_title _("Pipeline Schedules")
-- if show_new_nav? && can?(current_user, :create_pipeline_schedule, @project)
+- if can?(current_user, :create_pipeline_schedule, @project)
- content_for :breadcrumbs_extra do
= link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create'
@@ -20,7 +20,7 @@
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
- if can?(current_user, :create_pipeline_schedule, @project)
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
= link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do
%span= _('New schedule')
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index c7237cb96d8..cfdaf6d43bb 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -2,8 +2,7 @@
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
-- if show_new_nav?
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
+- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
%h3.page-title
= _("Schedule a new pipeline")
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 255d7ef38e0..d407e187df0 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -60,8 +60,21 @@
= f.check_box :public_builds
%strong Public pipelines
.help-block
- Allow everyone to access pipelines for public and internal projects
+ Allow public access to pipelines and job details, including output logs and artifacts
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
+ .bs-callout.bs-callout-info
+ %p If enabled:
+ %ul
+ %li
+ For public projects, anyone can view pipelines and access job details (output logs and artifacts)
+ %li
+ For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
+ %li
+ For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
+ %p
+ If disabled, the access level will depend on the user's
+ permissions in the project.
+
%hr
.form-group
.checkbox
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 8056217bb1e..3e2a24a4c32 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,8 +1,6 @@
- breadcrumb_title "Integrations"
- page_title @service.title, "Services"
-
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
+- add_to_breadcrumbs("Settings", edit_project_path(@project))
= render "projects/settings/head"
= render 'form'
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index ccc5fe80755..1803e7f7211 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Snippets"
-- if show_new_nav? && can?(current_user, :create_project_snippet, @project)
+- if can?(current_user, :create_project_snippet, @project)
- content_for :breadcrumbs_extra do
= link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet"
@@ -9,7 +9,7 @@
- include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls.visible-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet"
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index bf97cbc1f68..a6fe02fcae0 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,6 +1,7 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
- page_title "Tags"
+- add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head"
.flex-list{ class: container_class }
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 10e6c49ae9f..0ef9de5fed6 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36">
+<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36">
<path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
<path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
<path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg
new file mode 100644
index 00000000000..fd80fd0f651
--- /dev/null
+++ b/app/views/shared/icons/_caret_down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg>
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
index 5468545da2e..0f5be6e2bc8 100644
--- a/app/views/shared/icons/_mr_bold.svg
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -1,2 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
-
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg
new file mode 100644
index 00000000000..7263d924f1f
--- /dev/null
+++ b/app/views/shared/icons/_plus_square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg>
diff --git a/app/views/shared/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg
new file mode 100644
index 00000000000..156dfa11df1
--- /dev/null
+++ b/app/views/shared/icons/_todo_done.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg>
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index cb706d80f23..f16bc8dd430 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -9,7 +9,6 @@
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
-- elsif issuable.author
- / TODO: change back to else #36860
+- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index d8144a39b23..a38cd319e3c 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -37,15 +37,13 @@
%li.divider.droplab-item-ignore
- / TODO: remove condition #36860
- - if issuable.author
- %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
- %button.btn.btn-transparent
- = icon('check', class: 'icon')
- .description
- %strong.title Report abuse
- %p.text
- Report
- = display_issuable_type.pluralize
- that are abusive, inappropriate or spam.
+ %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
+ %button.btn.btn-transparent
+ = icon('check', class: 'icon')
+ .description
+ %strong.title Report abuse
+ %p.text
+ Report
+ = display_issuable_type.pluralize
+ that are abusive, inappropriate or spam.
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b07bc45512f..0afa48b392c 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar')
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { "offset-top" => ("50" unless show_new_nav?), "spy" => ("affix" unless show_new_nav?), signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
diff --git a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
new file mode 100644
index 00000000000..e48a5704fdd
--- /dev/null
+++ b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
@@ -0,0 +1,5 @@
+---
+title: Update gpg documentation with gpg2
+merge_request: 13851
+author: M M Arif
+type: other
diff --git a/changelogs/unreleased/36860-migrate-issues-author.yml b/changelogs/unreleased/36860-migrate-issues-author.yml
new file mode 100644
index 00000000000..3e9fcc55836
--- /dev/null
+++ b/changelogs/unreleased/36860-migrate-issues-author.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate issues authored by deleted user to the Ghost user
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/api-gpg-key-management.yml b/changelogs/unreleased/api-gpg-key-management.yml
new file mode 100644
index 00000000000..0be35a5823b
--- /dev/null
+++ b/changelogs/unreleased/api-gpg-key-management.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: Add GPG key management'
+merge_request: 13828
+author: Robert Schilling
+type: added
diff --git a/changelogs/unreleased/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml
new file mode 100644
index 00000000000..8195e97ed59
--- /dev/null
+++ b/changelogs/unreleased/fuzzy-issue-search.yml
@@ -0,0 +1,5 @@
+---
+title: Support a multi-word fuzzy seach issues/merge requests on search bar
+merge_request: 13780
+author: Hiroyuki Sato
+type: changed
diff --git a/changelogs/unreleased/sh-bump-jira-gem.yml b/changelogs/unreleased/sh-bump-jira-gem.yml
new file mode 100644
index 00000000000..d76b688caac
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-jira-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump jira-ruby gem to 1.4.1 to fix issues with HTTP proxies
+merge_request:
+author:
+type: fixed
diff --git a/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
new file mode 100644
index 00000000000..294141e4fdb
--- /dev/null
+++ b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
@@ -0,0 +1,36 @@
+class MigrateIssuesToGhostUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include ::EachBatch
+ end
+
+ def reset_column_in_migration_models
+ ActiveRecord::Base.clear_cache!
+
+ ::User.reset_column_information
+ end
+
+ def up
+ reset_column_in_migration_models
+
+ # we use the model method because rewriting it is too complicated and would require copying multiple methods
+ ghost_id = ::User.ghost.id
+
+ Issue.where('NOT EXISTS (?)', User.unscoped.select(1).where('issues.author_id = users.id')).each_batch do |relation|
+ relation.update_all(author_id: ghost_id)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
new file mode 100644
index 00000000000..ab6e9fb565a
--- /dev/null
+++ b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
@@ -0,0 +1,14 @@
+class AddForeignKeyToIssueAuthor < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:issues, :users, column: :author_id, on_delete: :nullify)
+ end
+
+ def down
+ remove_foreign_key(:issues, column: :author_id)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 40b84f2bddd..f980667a38f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170831195038) do
+ActiveRecord::Schema.define(version: 20170901071411) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1708,6 +1708,7 @@ ActiveRecord::Schema.define(version: 20170831195038) do
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
+ add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
diff --git a/doc/api/README.md b/doc/api/README.md
index c2a08dcff07..db61497db53 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,16 +61,7 @@ following locations:
## Road to GraphQL
-Going forward, we will start on moving to
-[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
-controller-specific endpoints. GraphQL has a number of benefits:
-
-1. We avoid having to maintain two different APIs.
-2. Callers of the API can request only what they need.
-3. It is versioned by default.
-
-It will co-exist with the current v4 REST API. If we have a v5 API, this should
-be a compatibility layer on top of GraphQL.
+We have changed our plans to move to GraphQL. After reviewing the GraphQL license, anything related to the Facebook BSD plus patent license will not be allowed at GitLab.
## Basic usage
@@ -246,8 +237,8 @@ The following table gives an overview of how the API functions generally behave.
| ------------ | ----------- |
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
-| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. |
-| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. |
+| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
+| `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. |
The following table shows the possible return codes for API requests.
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 5ca766bf87d..e8deb3e07e9 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -94,7 +94,7 @@ Example response:
## Delete an environment
-It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist.
+It returns `204` if the environment was successfully deleted, and `404` if the environment does not exist.
```
DELETE /projects/:id/environments/:environment_id
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 24c8ff5fa7a..ad2521230e6 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -95,8 +95,7 @@ Parameters:
## Delete snippet
-Deletes an existing project snippet. This is an idempotent function and deleting a non-existent
-snippet still returns a `200 OK` status code.
+Deletes an existing project snippet. This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /projects/:id/snippets/:snippet_id
diff --git a/doc/api/users.md b/doc/api/users.md
index 57a13eb477d..9f3e4caf2f4 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -299,10 +299,7 @@ e.g. when renaming the email address to some existing one.
## User deletion
Deletes a user. Available only for administrators.
-This is an idempotent function, calling this function for a non-existent user id
-still returns a status code `200 OK`.
-The JSON response differs if the user was actually deleted or not.
-In the former the user is returned and in the latter not.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /users/:id
@@ -524,8 +521,7 @@ Parameters:
## Delete SSH key for current user
Deletes key owned by currently authenticated user.
-This is an idempotent function and calling it on a key that is already deleted
-or not available results in `200 OK`.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /user/keys/:key_id
@@ -548,7 +544,216 @@ Parameters:
- `id` (required) - id of specified user
- `key_id` (required) - SSH key ID
-Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
+## List all GPG keys
+
+Get a list of currently authenticated user's GPG keys.
+
+```
+GET /user/gpg_keys
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key
+
+Get a specific GPG key of currently authenticated user.
+
+```
+GET /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key
+
+Creates a new GPG key owned by the currently authenticated user.
+
+```
+POST /user/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| key | string | yes | The new GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key
+
+Delete a GPG key owned by currently authenticated user.
+
+```
+DELETE /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Returns `204 No Content` on success, or `404 Not found` if the key cannot be found.
+
+## List all GPG keys for given user
+
+Get a list of a specified user's GPG keys. Available only for admins.
+
+```
+GET /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key for a given user
+
+Get a specific GPG key for a given user. Available only for admins.
+
+```
+GET /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key for a given user
+
+Create new GPG key owned by the specified user. Available only for admins.
+
+```
+POST /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key for a given user
+
+Delete a GPG key owned by a specified user. Available only for admins.
+
+```
+DELETE /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
## List emails
@@ -654,8 +859,7 @@ Parameters:
## Delete email for current user
Deletes email owned by currently authenticated user.
-This is an idempotent function and calling it on a email that is already deleted
-or not available results in `200 OK`.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /user/emails/:email_id
@@ -678,8 +882,6 @@ Parameters:
- `id` (required) - id of specified user
- `email_id` (required) - email ID
-Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
-
## Block user
Blocks the specified user. Available only for admin.
diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png
index 82e0dd8e85e..355be80ecb6 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_system_notes.png
+++ b/doc/user/project/issues/img/confidential_issues_system_notes.png
Binary files differ
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 3ff5a08d72c..dbc1305101f 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -66,10 +66,30 @@ in the pipelines settings page.
## Visibility of pipelines
-For public and internal projects, the pipelines page can be accessed by
-anyone and those logged in respectively. If you wish to hide it so that only
-the members of the project or group have access to it, uncheck the **Public
-pipelines** checkbox and save the changes.
+Access to pipelines and job details (including output of logs and artifacts)
+is checked against your current user access level and the **Public pipelines**
+project setting.
+
+If **Public pipelines** is enabled (default):
+
+- for **public** projects, anyone can view the pipelines and access the job details
+ (output logs and artifacts)
+- for **internal** projects, any logged in user can view the pipelines
+ and access the job details
+ (output logs and artifacts)
+- for **private** projects, any member (guest or higher) can view the pipelines
+ and access the job details
+ (output logs and artifacts)
+
+If **Public pipelines** is disabled:
+
+- for **public** projects, anyone can view the pipelines, but only members
+ (reporter or higher) can access the job details (output logs and artifacts)
+- for **internal** projects, any logged in user can view the pipelines,
+ but only members (reporter or higher) can access the job details (output logs
+ and artifacts)
+- for **private** projects, only members (reporter or higher)
+ can view the pipelines and access the job details (output logs and artifacts)
## Auto-cancel pending pipelines
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index afe8066d408..20aadb8f7ff 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -31,6 +31,16 @@ to be met:
## Generating a GPG key
+>**Notes:**
+- If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in
+ the following commands.
+- If Git is using `gpg` and you get errors like `secret key not available` or
+ `gpg: signing failed: secret key not available`, run the following command to
+ change to `gpg2`:
+ ```
+ git config --global gpg.program gpg2
+ ```
+
If you don't already have a GPG key, the following steps will help you get
started:
diff --git a/doc/user/search/img/issue_search_by_term.png b/doc/user/search/img/issue_search_by_term.png
new file mode 100644
index 00000000000..3cefa3adb8b
--- /dev/null
+++ b/doc/user/search/img/issue_search_by_term.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index f5c7ce49e8e..21e96d8b11c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -40,6 +40,20 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+### Searching for specific terms
+
+You can filter issues and merge requests by specific terms included in titles or descriptions.
+
+* Syntax
+ * Searches look for all the words in a query, in any order. E.g.: searching
+ issues for `display bug` will return all issues matching both those words, in any order.
+ * To find the exact term, use double quotes: `"display bug"`
+* Limitation
+ * For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
+ issues for `included in titles` is same as `included titles`
+
+![filter issues by specific terms](img/issue_search_by_term.png)
+
### Issues and merge requests per group
Similar to **Issues and merge requests per project**, you can also search for issues
diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb
new file mode 100644
index 00000000000..3cd5f4ce497
--- /dev/null
+++ b/features/support/gitaly.rb
@@ -0,0 +1,3 @@
+Spinach.hooks.before_scenario do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0092cc14e74..031dd02c6eb 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -491,6 +491,10 @@ module API
expose :user, using: Entities::UserPublic
end
+ class GPGKey < Grape::Entity
+ expose :id, :key, :created_at
+ end
+
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 96f47bb618a..1825c90a23b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -233,6 +233,86 @@ module API
destroy_conditionally!(key)
end
+ desc 'Add a GPG key to a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ key = user.gpg_keys.new(declared_params(include_missing: false))
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Get the GPG keys of a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present paginate(user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Delete an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ delete ':id/gpg_keys/:key_id' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ status 204
+ key.destroy
+ end
+
+ desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post ':id/gpg_keys/:key_id/revoke' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
desc 'Add an email address to a specified user. Available only for admins.' do
success Entities::Email
end
@@ -492,6 +572,76 @@ module API
destroy_conditionally!(key)
end
+ desc "Get the currently authenticated user's GPG keys" do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ use :pagination
+ end
+ get 'gpg_keys' do
+ present paginate(current_user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Get a single GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ get 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ present key, with: Entities::GPGKey
+ end
+
+ desc 'Add a new GPG key to the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post 'gpg_keys' do
+ key = current_user.gpg_keys.new(declared_params)
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Revoke a GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post 'gpg_keys/:key_id/revoke' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
+ desc 'Delete a GPG key from the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ status 204
+ key.destroy
+ end
+
desc "Get the currently authenticated user's email addresses" do
success Entities::Email
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index f7577b02d5d..75d4efc0bc5 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -134,15 +134,19 @@ module Gitlab
# This is to work around a bug in libgit2 that causes in-memory refs to
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/474
def find_branch(name, force_reload = false)
- reload_rugged if force_reload
+ gitaly_migrate(:find_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.find_branch(name)
+ else
+ reload_rugged if force_reload
- rugged_ref = rugged.branches[name]
- if rugged_ref
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rugged_ref = rugged.branches[name]
+ if rugged_ref
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8c0008c6971..a1a25cf2079 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -78,6 +78,20 @@ module Gitlab
raise ArgumentError, e.message
end
+ def find_branch(branch_name)
+ request = Gitaly::DeleteBranchRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(branch_name)
+ )
+
+ response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
+ branch = response.branch
+ return unless branch
+
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
+ end
+
private
def consume_refs_response(response)
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index b42bc67ccfc..7c2d1d8f887 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -4,6 +4,7 @@ module Gitlab
extend ActiveSupport::Concern
MIN_CHARS_FOR_PARTIAL_MATCHING = 3
+ REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/
class_methods do
def to_pattern(query)
@@ -17,6 +18,28 @@ module Gitlab
def partial_matching?(query)
query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
end
+
+ def to_fuzzy_arel(column, query)
+ words = select_fuzzy_words(query)
+
+ matches = words.map { |word| arel_table[column].matches(to_pattern(word)) }
+
+ matches.reduce { |result, match| result.and(match) }
+ end
+
+ def select_fuzzy_words(query)
+ quoted_words = query.scan(REGEX_QUOTED_WORD)
+
+ query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') }
+
+ words = query.split(/\s+/)
+
+ quoted_words.map! { |quoted_word| quoted_word[1..-2] }
+
+ words.concat(quoted_words)
+
+ words.select { |word| partial_matching?(word) }
+ end
end
end
end
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
index 015c7ed1731..53a47eb0f42 100644
--- a/lib/system_check/app/init_script_up_to_date_check.rb
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -7,26 +7,22 @@ module SystemCheck
set_skip_reason 'skipped (omnibus-gitlab has no init script)'
def skip?
- omnibus_gitlab?
- end
+ return true if omnibus_gitlab?
- def multi_check
- recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
+ unless init_file_exists?
+ self.skip_reason = "can't check because of previous errors"
- unless File.exist?(SCRIPT_PATH)
- $stdout.puts "can't check because of previous errors".color(:magenta)
- return
+ true
end
+ end
+
+ def check?
+ recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
recipe_content = File.read(recipe_path)
script_content = File.read(SCRIPT_PATH)
- if recipe_content == script_content
- $stdout.puts 'yes'.color(:green)
- else
- $stdout.puts 'no'.color(:red)
- show_error
- end
+ recipe_content == script_content
end
def show_error
@@ -38,6 +34,12 @@ module SystemCheck
)
fix_and_rerun
end
+
+ private
+
+ def init_file_exists?
+ File.exist?(SCRIPT_PATH)
+ end
end
end
end
diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb
index 7f9e2ffffc2..0f5742dd67f 100644
--- a/lib/system_check/base_check.rb
+++ b/lib/system_check/base_check.rb
@@ -62,6 +62,25 @@ module SystemCheck
call_or_return(@skip_reason) || 'skipped'
end
+ # Define a reason why we skipped the SystemCheck (during runtime)
+ #
+ # This is used when you need dynamic evaluation like when you have
+ # multiple reasons why a check can fail
+ #
+ # @param [String] reason to be displayed
+ def skip_reason=(reason)
+ @skip_reason = reason
+ end
+
+ # Skip reason defined during runtime
+ #
+ # This value have precedence over the one defined in the subclass
+ #
+ # @return [String] the reason
+ def skip_reason
+ @skip_reason
+ end
+
# Does the check support automatically repair routine?
#
# @return [Boolean] whether check implemented `#repair!` method or not
diff --git a/lib/system_check/incoming_email/foreman_configured_check.rb b/lib/system_check/incoming_email/foreman_configured_check.rb
new file mode 100644
index 00000000000..1db7bf2b782
--- /dev/null
+++ b/lib/system_check/incoming_email/foreman_configured_check.rb
@@ -0,0 +1,23 @@
+module SystemCheck
+ module IncomingEmail
+ class ForemanConfiguredCheck < SystemCheck::BaseCheck
+ set_name 'Foreman configured correctly?'
+
+ def check?
+ path = Rails.root.join('Procfile')
+
+ File.exist?(path) && File.read(path) =~ /^mail_room:/
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Enable mail_room in your Procfile.'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb
new file mode 100644
index 00000000000..dee108d987b
--- /dev/null
+++ b/lib/system_check/incoming_email/imap_authentication_check.rb
@@ -0,0 +1,45 @@
+module SystemCheck
+ module IncomingEmail
+ class ImapAuthenticationCheck < SystemCheck::BaseCheck
+ set_name 'IMAP server credentials are correct?'
+
+ def check?
+ if mailbox_config
+ begin
+ imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
+ imap.starttls if config[:start_tls]
+ imap.login(config[:email], config[:password])
+ connected = true
+ rescue
+ connected = false
+ end
+ end
+
+ connected
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Check that the information in config/gitlab.yml is correct'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mailbox_config
+ return @config if @config
+
+ config_path = Rails.root.join('config', 'mail_room.yml').to_s
+ erb = ERB.new(File.read(config_path))
+ erb.filename = config_path
+ config_file = YAML.load(erb.result)
+
+ @config = config_file[:mailboxes]&.first
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/initd_configured_check.rb b/lib/system_check/incoming_email/initd_configured_check.rb
new file mode 100644
index 00000000000..ea23b8ef49c
--- /dev/null
+++ b/lib/system_check/incoming_email/initd_configured_check.rb
@@ -0,0 +1,32 @@
+module SystemCheck
+ module IncomingEmail
+ class InitdConfiguredCheck < SystemCheck::BaseCheck
+ set_name 'Init.d configured correctly?'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def check?
+ mail_room_configured?
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Enable mail_room in the init.d configuration.'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mail_room_configured?
+ path = '/etc/default/gitlab'
+ File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/mail_room_running_check.rb b/lib/system_check/incoming_email/mail_room_running_check.rb
new file mode 100644
index 00000000000..c1807501829
--- /dev/null
+++ b/lib/system_check/incoming_email/mail_room_running_check.rb
@@ -0,0 +1,43 @@
+module SystemCheck
+ module IncomingEmail
+ class MailRoomRunningCheck < SystemCheck::BaseCheck
+ set_name 'MailRoom running?'
+
+ def skip?
+ return true if omnibus_gitlab?
+
+ unless mail_room_configured?
+ self.skip_reason = "can't check because of previous errors"
+ true
+ end
+ end
+
+ def check?
+ mail_room_running?
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab('RAILS_ENV=production bin/mail_room start')
+ )
+ for_more_information(
+ see_installation_guide_section('Install Init Script'),
+ 'see log/mail_room.log for possible errors'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mail_room_configured?
+ path = '/etc/default/gitlab'
+ File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
+ end
+
+ def mail_room_running?
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
+ ps_ux.include?("mail_room")
+ end
+ end
+ end
+end
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
index 6604b1078cf..00221f77cf4 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -23,7 +23,7 @@ module SystemCheck
#
# @param [BaseCheck] check class
def <<(check)
- raise ArgumentError unless check < BaseCheck
+ raise ArgumentError unless check.is_a?(Class) && check < BaseCheck
@checks << check
end
@@ -48,7 +48,7 @@ module SystemCheck
# When implements skip method, we run it first, and if true, skip the check
if check.can_skip? && check.skip?
- $stdout.puts check_klass.skip_reason.color(:magenta)
+ $stdout.puts check.skip_reason.try(:color, :magenta) || check_klass.skip_reason.color(:magenta)
return
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 92a3f503fcb..654f638c454 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -309,133 +309,24 @@ namespace :gitlab do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
warn_user_is_not_gitlab
- start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled
- check_imap_authentication
+ checks = [
+ SystemCheck::IncomingEmail::ImapAuthenticationCheck
+ ]
if Rails.env.production?
- check_initd_configured_correctly
- check_mail_room_running
+ checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
+ checks << SystemCheck::IncomingEmail::MailRoomRunningCheck
else
- check_foreman_configured_correctly
+ checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck
end
- else
- puts 'Reply by email is disabled in config/gitlab.yml'
- end
-
- finished_checking "Reply by email"
- end
-
- # Checks
- ########################
-
- def check_initd_configured_correctly
- return if omnibus_gitlab?
-
- print "Init.d configured correctly? ... "
-
- path = "/etc/default/gitlab"
-
- if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Enable mail_room in the init.d configuration."
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
- end
- end
-
- def check_foreman_configured_correctly
- print "Foreman configured correctly? ... "
- path = Rails.root.join("Procfile")
-
- if File.exist?(path) && File.read(path) =~ /^mail_room:/
- puts "yes".color(:green)
+ SystemCheck.run('Reply by email', checks)
else
- puts "no".color(:red)
- try_fixing_it(
- "Enable mail_room in your Procfile."
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
- end
- end
-
- def check_mail_room_running
- return if omnibus_gitlab?
-
- print "MailRoom running? ... "
-
- path = "/etc/default/gitlab"
-
- unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "can't check because of previous errors".color(:magenta)
- return
- end
-
- if mail_room_running?
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- sudo_gitlab("RAILS_ENV=production bin/mail_room start")
- )
- for_more_information(
- see_installation_guide_section("Install Init Script"),
- "see log/mail_room.log for possible errors"
- )
- fix_and_rerun
- end
- end
-
- def check_imap_authentication
- print "IMAP server credentials are correct? ... "
-
- config_path = Rails.root.join('config', 'mail_room.yml').to_s
- erb = ERB.new(File.read(config_path))
- erb.filename = config_path
- config_file = YAML.load(erb.result)
-
- config = config_file[:mailboxes].first
-
- if config
- begin
- imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
- imap.starttls if config[:start_tls]
- imap.login(config[:email], config[:password])
- connected = true
- rescue
- connected = false
- end
- end
-
- if connected
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Check that the information in config/gitlab.yml is correct"
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
+ puts 'Reply by email is disabled in config/gitlab.yml'
end
end
-
- def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
- ps_ux.include?("mail_room")
- end
end
namespace :ldap do
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index a6ad5981f8f..c480b5b7e34 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do
let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) }
- let!(:issue) { create(:issue, project: project) }
- let!(:issue2) { create(:issue, project: project) }
+ let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
+ let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
project.team << [user, :master]
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 913258ca40f..e010b5f3444 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
- let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
- let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
- let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
- let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
- let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
- let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
- let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
+ let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
+ let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
+ let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
+ let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit project_board_path(project, board)
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index c470cb7c716..28b636f9359 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -40,18 +40,4 @@ feature 'Issue Detail', :js do
end
end
end
-
- context 'when authored by a user who is later deleted' do
- before do
- issue.update_attribute(:author_id, nil)
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'shows the issue' do
- page.within('.issuable-details') do
- expect(find('h2')).to have_content(issue.title)
- end
- end
- end
end
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 10fcc590c89..dcb8dbce178 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -4,7 +4,10 @@ import '~/gl_dropdown';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
-(() => {
+describe('glDropdown', function describeDropdown() {
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
+
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
@@ -39,187 +42,217 @@ import '~/lib/utils/url_utility';
remoteCallback = callback.bind({}, data);
};
- describe('Dropdown', function describeDropdown() {
- preloadFixtures('static/gl_dropdown.html.raw');
- loadJSONFixtures('projects.json');
-
- function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
- const options = Object.assign({
- selectable: true,
- filterable: isFilterable,
- data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
- search: {
- fields: ['name']
- },
- text: project => (project.name_with_namespace || project.name),
- id: project => project.id,
- }, extraOpts);
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
- }
+ function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
+ const options = Object.assign({
+ selectable: true,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
+ text: project => (project.name_with_namespace || project.name),
+ id: project => project.id,
+ }, extraOpts);
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
+ }
+
+ beforeEach(() => {
+ loadFixtures('static/gl_dropdown.html.raw');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = getJSONFixture('projects.json');
+ });
- beforeEach(() => {
- loadFixtures('static/gl_dropdown.html.raw');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
- });
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
- afterEach(() => {
- $('body').unbind('keydown');
- this.dropdownContainerElement.unbind('keyup');
- });
+ it('should open on click', () => {
+ initDropDown.call(this, false);
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
- it('should open on click', () => {
- initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
- });
+ it('escapes HTML as text', () => {
+ this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
- it('escapes HTML as text', () => {
- this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
+ initDropDown.call(this, false);
- initDropDown.call(this, false);
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('<script>alert("testing");</script>');
+ });
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('<script>alert("testing");</script>');
- });
+ it('should output HTML when highlighting', () => {
+ this.projectsData[0].name_with_namespace = 'testing';
+ $('.dropdown-input .dropdown-input-field').val('test');
- it('should output HTML when highlighting', () => {
- this.projectsData[0].name_with_namespace = 'testing';
- $('.dropdown-input .dropdown-input-field').val('test');
+ initDropDown.call(this, false, true, {
+ highlight: true,
+ });
- initDropDown.call(this, false, true, {
- highlight: true,
- });
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('testing');
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('testing');
+ expect(
+ $('.dropdown-content li:first-child a').html(),
+ ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ });
- expect(
- $('.dropdown-content li:first-child a').html(),
- ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ describe('that is open', () => {
+ beforeEach(() => {
+ initDropDown.call(this, false, false);
+ this.dropdownButtonElement.click();
});
- describe('that is open', () => {
- beforeEach(() => {
- initDropDown.call(this, false, false);
- this.dropdownButtonElement.click();
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
+ });
- it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
- navigateWithKeys('down', randomIndex, () => {
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
+ });
- it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
- navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
- });
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(gl.utils, 'visitUrl').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ const linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
+ });
- it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
- navigateWithKeys('down', randomIndex, () => {
- spyOn(gl.utils, 'visitUrl').and.stub();
- navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
- expect(link).toHaveClass('is-active');
- const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
- });
- });
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
- it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should show loading indicator while search results are being fetched by backend', () => {
+ const dropdownMenu = document.querySelector('.dropdown-menu');
+
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
+ remoteCallback();
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
+ });
+
+ it('should not focus search input while remote task is not complete', () => {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', () => {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time after transition', () => {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', () => {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ it('should still have input value on close and restore', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
+
+ describe('renderItem', () => {
+ describe('without selected value', () => {
+ let dropdown;
- describe('opened and waiting for a remote callback', () => {
beforeEach(() => {
- initDropDown.call(this, true, true);
- this.dropdownButtonElement.click();
+ const dropdownOptions = {
+
+ };
+ const $dropdownDiv = $('<div />');
+ $dropdownDiv.glDropdown(dropdownOptions);
+ dropdown = $dropdownDiv.data('glDropdown');
});
- it('should show loading indicator while search results are being fetched by backend', () => {
- const dropdownMenu = document.querySelector('.dropdown-menu');
+ it('marks items without ID as active', () => {
+ const dummyData = { };
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
- remoteCallback();
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).toHaveClass('is-active');
});
- it('should focus search input after remote task is complete', () => {
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
+ it('does not mark items with ID as active', () => {
+ const dummyData = {
+ id: 'ea'
+ };
- it('should focus on input when opening for the second time after transition', () => {
- remoteCallback();
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', () => {
- initDropDown.call(this, false, true);
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).not.toHaveClass('is-active');
});
});
-
- it('should still have input value on close and restore', () => {
- const $searchInput = $(SEARCH_INPUT_SELECTOR);
- initDropDown.call(this, false, true);
- $searchInput
- .trigger('focus')
- .val('g')
- .trigger('input');
- expect($searchInput.val()).toEqual('g');
- this.dropdownButtonElement.trigger('hidden.bs.dropdown');
- $searchInput
- .trigger('blur')
- .trigger('focus');
- expect($searchInput.val()).toEqual('g');
- });
});
-})();
+});
diff --git a/spec/javascripts/monitoring/graph_row_spec.js b/spec/javascripts/monitoring/graph_row_spec.js
deleted file mode 100644
index 6a79d7c8f82..00000000000
--- a/spec/javascripts/monitoring/graph_row_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Vue from 'vue';
-import GraphRow from '~/monitoring/components/graph_row.vue';
-import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
-import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(GraphRow);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-describe('GraphRow', () => {
- beforeEach(() => {
- spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
- });
- describe('Computed props', () => {
- it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
- const component = createComponent({
- rowData: convertedMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.bootstrapClass).toEqual('col-md-6');
- });
-
- it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
- const component = createComponent({
- rowData: [convertedMetrics[0]],
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.bootstrapClass).toEqual('col-md-12');
- });
- });
-
- it('has one column', () => {
- const component = createComponent({
- rowData: convertedMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.$el.querySelectorAll('.prometheus-svg-container').length)
- .toEqual(component.rowData.length);
- });
-
- it('has two columns', () => {
- const component = createComponent({
- rowData: convertedMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.$el.querySelectorAll('.col-md-6').length)
- .toEqual(component.rowData.length);
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index 20c1e6a0005..88aa7659275 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -5,10 +5,10 @@ describe('MonitoringStore', () => {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
- it('contains one group that contains two queries sorted by priority in one row', () => {
+ it('contains one group that contains two queries sorted by priority', () => {
expect(this.store.groups).toBeDefined();
expect(this.store.groups.length).toEqual(1);
- expect(this.store.groups[0].metrics.length).toEqual(1);
+ expect(this.store.groups[0].metrics.length).toEqual(2);
});
it('gets the metrics count for every group', () => {
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 4cfb4b7d357..08959e7bc16 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -916,27 +916,37 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#find_branch' do
- it 'should return a Branch for master' do
- branch = repository.find_branch('master')
+ shared_examples 'finding a branch' do
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
- it 'should handle non-existent branch' do
- branch = repository.find_branch('this-is-garbage')
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
- expect(branch).to eq(nil)
+ expect(branch).to eq(nil)
+ end
end
- it 'should reload Rugged::Repository and return master' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
+ context 'when Gitaly find_branch feature is enabled' do
+ it_behaves_like 'finding a branch'
+ end
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'finding a branch'
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
+
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8da02b0cf00..beed4e77e8b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -264,6 +264,7 @@ project:
- statistics
- container_repositories
- uploads
+- members_and_requesters
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 9d7b2136dab..48d56628ed5 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do
end
end
end
+
+ describe '.select_fuzzy_words' do
+ subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns array cotaining a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns empty array' do
+ expect(select_fuzzy_words).to match_array([])
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words divided by two spaces both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words equal to 3 chars and shorter than 3 chars' do
+ let(:query) { 'foo ba' }
+
+ it 'returns array containing a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote' do
+ let(:query) { '"really bar"' }
+
+ it 'returns array containing a multi-word' do
+ expect(select_fuzzy_words).to match_array(['really bar'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns array containing a multi-word and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do
+ let(:query) { 'foo"really bar"' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['foo"really', 'bar"'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do
+ let(:query) { '"really bar"baz' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['"really', 'bar"baz'])
+ end
+ end
+
+ context 'with two multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz "awesome feature"' }
+
+ it 'returns array containing two multi-words and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
+ end
+ end
+ end
+
+ describe '.to_fuzzy_arel' do
+ subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns a single ILIKE condition' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/)
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns nil' do
+ expect(to_fuzzy_arel).to be_nil
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
+ end
+ end
+ end
end
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
index 4de5da984ba..9da3648400e 100644
--- a/spec/lib/system_check/simple_executor_spec.rb
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do
end
end
+ class DynamicSkipCheck < SystemCheck::BaseCheck
+ set_name 'dynamic skip check'
+ set_skip_reason 'this is a skip reason'
+
+ def skip?
+ self.skip_reason = 'this is a dynamic skip reason'
+ true
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
class MultiCheck < SystemCheck::BaseCheck
set_name 'multi check'
@@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do
expect(subject.checks.size).to eq(1)
end
+
+ it 'errors out when passing multiple items' do
+ expect { subject << [SimpleCheck, OtherCheck] }.to raise_error(ArgumentError)
+ end
end
subject { described_class.new('Test') }
@@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do
subject.run_check(SkipCheck)
end
- it 'displays #skip_reason' do
+ it 'displays .skip_reason' do
expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout
end
+ it 'displays #skip_reason' do
+ expect { subject.run_check(DynamicSkipCheck) }.to output(/this is a dynamic skip reason/).to_stdout
+ end
+
it 'does not execute #check when #skip? is true' do
expect_any_instance_of(SkipCheck).not_to receive(:check?)
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
new file mode 100644
index 00000000000..cfd4021fbac
--- /dev/null
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb')
+
+describe MigrateIssuesToGhostUser, :migration do
+ describe '#up' do
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:users) { table(:users) }
+
+ before do
+ projects.create!(name: 'gitlab')
+ user = users.create(email: 'test@example.com')
+ issues.create(title: 'Issue 1', author_id: nil, project_id: 1)
+ issues.create(title: 'Issue 2', author_id: user.id, project_id: 1)
+ end
+
+ context 'when ghost user exists' do
+ let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') }
+
+ it 'does not create a new user' do
+ expect { schema_migrate_up! }.not_to change { User.count }
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+
+ context 'when ghost user does not exist' do
+ it 'creates a new user' do
+ expect { schema_migrate_up! }.to change { User.count }.by(1)
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(User.ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index dfbe1a7c192..37f6fd3a25b 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -66,56 +66,76 @@ describe Issuable do
end
describe ".search" do
- let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
+ let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe ".full_search" do
let!(:searchable_issue) do
- create(:issue, title: "Searchable issue", description: 'kittens')
+ create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.full_search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description' do
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns issues with a matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching description' do
+ it 'returns issues with a partially matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description regardless of the casing' do
+ it 'returns issues with a matching description regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.description.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching description' do
+ expect(issuable_class.full_search('many kittens')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe '.to_ability_name' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f9cd12c0ff3..f36d6eeb327 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -9,6 +9,7 @@ describe Group do
it { is_expected.to have_many(:users).through(:group_members) }
it { is_expected.to have_many(:owners).through(:group_members) }
it { is_expected.to have_many(:requesters).dependent(:destroy) }
+ it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
@@ -25,22 +26,8 @@ describe Group do
group.add_developer(developer)
end
- describe '#members' do
- it 'includes members and exclude requesters' do
- member_user_ids = group.members.pluck(:user_id)
-
- expect(member_user_ids).to include(developer.id)
- expect(member_user_ids).not_to include(requester.id)
- end
- end
-
- describe '#requesters' do
- it 'does not include requesters' do
- requester_user_ids = group.requesters.pluck(:user_id)
-
- expect(requester_user_ids).to include(requester.id)
- expect(requester_user_ids).not_to include(developer.id)
- end
+ it_behaves_like 'members and requesters associations' do
+ let(:namespace) { group }
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 87513e18b25..a07ce05a865 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -409,6 +409,15 @@ describe Member do
expect(members).to be_a Array
expect(members).to be_empty
end
+
+ it 'supports differents formats' do
+ list = ['joe@local.test', admin, user1.id, user2.id.to_s]
+
+ members = described_class.add_users(source, list, :master)
+
+ expect(members.size).to eq(4)
+ expect(members.first).to be_invite
+ end
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index be1ae295f75..1f7c6a82b91 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -74,6 +74,7 @@ describe Project do
it { is_expected.to have_many(:forks).through(:forked_project_links) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
+ it { is_expected.to have_many(:members_and_requesters) }
context 'after initialized' do
it "has a project_feature" do
@@ -90,22 +91,8 @@ describe Project do
project.team << [developer, :developer]
end
- describe '#members' do
- it 'includes members and exclude requesters' do
- member_user_ids = project.members.pluck(:user_id)
-
- expect(member_user_ids).to include(developer.id)
- expect(member_user_ids).not_to include(requester.id)
- end
- end
-
- describe '#requesters' do
- it 'does not include requesters' do
- requester_user_ids = project.requesters.pluck(:user_id)
-
- expect(requester_user_ids).to include(requester.id)
- expect(requester_user_ids).not_to include(developer.id)
- end
+ it_behaves_like 'members and requesters associations' do
+ let(:namespace) { project }
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 5fef4437997..37cb95a16e3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4,6 +4,7 @@ describe API::Users do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
+ let(:gpg_key) { create(:gpg_key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
@@ -753,6 +754,164 @@ describe API::Users do
end
end
+ describe 'POST /users/:id/keys' do
+ before do
+ admin
+ end
+
+ it 'does not create invalid GPG key' do
+ post api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'creates GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api("/users/#{user.id}/gpg_keys", admin), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns 400 for invalid ID' do
+ post api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'GET /user/:id/gpg_keys' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api("/users/#{user.id}/gpg_keys")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+ end
+ end
+
+ describe 'DELETE /user/:id/gpg_keys/:key_id' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api("/users/#{user.id}/keys/42")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'deletes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.keys << key
+ user.save
+
+ delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
+ describe 'POST /user/:id/gpg_keys/:key_id/revoke' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'revokes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
describe "POST /users/:id/emails" do
before do
admin
@@ -1153,6 +1312,173 @@ describe API::Users do
end
end
+ describe 'GET /user/gpg_keys' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/user/gpg_keys')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+
+ context 'scopes' do
+ let(:path) { '/user/gpg_keys' }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+ end
+
+ describe 'GET /user/gpg_keys/:key_id' do
+ it 'returns a single key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['key']).to eq(gpg_key.key)
+ end
+
+ it 'returns 404 Not Found within invalid ID' do
+ get api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it "returns 404 error if admin accesses user's GPG key" do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 404 for invalid ID' do
+ get api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'scopes' do
+ let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+
+ describe 'POST /user/gpg_keys' do
+ it 'creates a GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api('/user/gpg_keys', user), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns a 401 error if unauthorized' do
+ post api('/user/gpg_keys'), key: 'some key'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'does not create GPG key without key' do
+ post api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+ end
+
+ describe 'POST /user/gpg_keys/:key_id/revoke' do
+ it 'revokes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ post api('/user/gpg_keys/42/revoke', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ post api('/users/gpg_keys/ASDF/revoke', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /user/gpg_keys/:key_id' do
+ it 'deletes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ delete api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ delete api("/user/gpg_keys/#{gpg_key.id}")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ delete api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
describe "GET /user/emails" do
context "when unauthenticated" do
it "returns authentication error" do
diff --git a/spec/support/group_members_shared_example.rb b/spec/support/group_members_shared_example.rb
new file mode 100644
index 00000000000..547c83c7955
--- /dev/null
+++ b/spec/support/group_members_shared_example.rb
@@ -0,0 +1,27 @@
+RSpec.shared_examples 'members and requesters associations' do
+ describe '#members_and_requesters' do
+ it 'includes members and requesters' do
+ member_and_requester_user_ids = namespace.members_and_requesters.pluck(:user_id)
+
+ expect(member_and_requester_user_ids).to include(requester.id, developer.id)
+ end
+ end
+
+ describe '#members' do
+ it 'includes members and exclude requesters' do
+ member_user_ids = namespace.members.pluck(:user_id)
+
+ expect(member_user_ids).to include(developer.id)
+ expect(member_user_ids).not_to include(requester.id)
+ end
+ end
+
+ describe '#requesters' do
+ it 'does not include requesters' do
+ requester_user_ids = namespace.requesters.pluck(:user_id)
+
+ expect(requester_user_ids).to include(requester.id)
+ expect(requester_user_ids).not_to include(developer.id)
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index faea2505e40..b17bc6692f3 100644
--- a/spec/views/layouts/nav/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
-describe 'layouts/nav/_project' do
+describe 'layouts/nav/sidebar/_project' do
describe 'container registry tab' do
before do
+ project = create(:project, :repository)
stub_container_registry_config(enabled: true)
- assign(:project, create(:project, :repository))
+ assign(:project, project)
+ assign(:repository, project.repository)
allow(view).to receive(:current_ref).and_return('master')
allow(view).to receive(:can?).and_return(true)