summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-07-28 15:26:00 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-07-28 15:26:00 +0100
commitdc04fdc1a3165134fc9245e9a591daceee857ef4 (patch)
treee3b01475f2e224cd1595167c9a5dd0d0dc013a89 /app
parentec302c6c0cf7d4b3a19bea43ce380dbaa60b363d (diff)
parent48c51e207e4cba8a69e4ca65cba1e169d384cefa (diff)
downloadgitlab-ce-dc04fdc1a3165134fc9245e9a591daceee857ef4.tar.gz
Merge remote-tracking branch 'origin/master' into ide
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/commons/bootstrap.js1
-rw-r--r--app/assets/javascripts/copy_as_gfm.js10
-rw-r--r--app/assets/javascripts/dispatcher.js74
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js5
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js5
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js26
-rw-r--r--app/assets/javascripts/gpg_badges.js15
-rw-r--r--app/assets/javascripts/how_to_merge.js12
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js18
-rw-r--r--app/assets/javascripts/init_legacy_filters.js15
-rw-r--r--app/assets/javascripts/init_notes.js14
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js2
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js13
-rw-r--r--app/assets/javascripts/issuable_context.js6
-rw-r--r--app/assets/javascripts/lazy_loader.js76
-rw-r--r--app/assets/javascripts/main.js32
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js3
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/project.js29
-rw-r--r--app/assets/javascripts/protected_branches/index.js9
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js53
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js106
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js39
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js114
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js28
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js5
-rw-r--r--app/assets/javascripts/protected_tags/index.js11
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js3
-rw-r--r--app/assets/javascripts/snippets_list.js9
-rw-r--r--app/assets/javascripts/star.js6
-rw-r--r--app/assets/javascripts/todos.js4
-rw-r--r--app/assets/javascripts/users/activity_calendar.js239
-rw-r--r--app/assets/javascripts/users/index.js24
-rw-r--r--app/assets/javascripts/users/user.js34
-rw-r--r--app/assets/javascripts/users/user_tabs.js177
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js11
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss12
-rw-r--r--app/assets/stylesheets/framework/files.scss10
-rw-r--r--app/assets/stylesheets/framework/filters.scss3
-rw-r--r--app/assets/stylesheets/framework/header.scss29
-rw-r--r--app/assets/stylesheets/framework/mixins.scss26
-rw-r--r--app/assets/stylesheets/framework/nav.scss14
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/new_nav.scss23
-rw-r--r--app/assets/stylesheets/new_sidebar.scss7
-rw-r--r--app/assets/stylesheets/pages/boards.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss66
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/assets/stylesheets/pages/profile.scss23
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss21
-rw-r--r--app/controllers/admin/application_settings_controller.rb81
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb15
-rw-r--r--app/controllers/application_controller.rb10
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb47
-rw-r--r--app/controllers/projects/application_controller.rb1
-rw-r--r--app/controllers/projects/badges_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb40
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb15
-rw-r--r--app/controllers/users_controller.rb5
-rw-r--r--app/finders/admin/projects_finder.rb33
-rw-r--r--app/finders/issuable_finder.rb1
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/helpers/application_settings_helper.rb85
-rw-r--r--app/helpers/avatars_helper.rb9
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb10
-rw-r--r--app/helpers/issues_helper.rb26
-rw-r--r--app/helpers/lazy_image_tag_helper.rb24
-rw-r--r--app/helpers/notes_helper.rb10
-rw-r--r--app/helpers/system_note_helper.rb3
-rw-r--r--app/helpers/triggers_helper.rb2
-rw-r--r--app/helpers/version_check_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb12
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/chat_team.rb9
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb3
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/ci/pipeline_variable.rb10
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/protected_ref.rb17
-rw-r--r--app/models/gpg_key.rb107
-rw-r--r--app/models/gpg_signature.rb21
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/merge_request_diff.rb17
-rw-r--r--app/models/merge_request_diff_file.rb10
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/issue_tracker_service.rb8
-rw-r--r--app/models/project_services/jira_service.rb28
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/system_note_metadata.rb3
-rw-r--r--app/models/user.rb6
-rw-r--r--app/models/wiki_page.rb21
-rw-r--r--app/policies/ci/build_policy.rb8
-rw-r--r--app/policies/ci/pipeline_policy.rb12
-rw-r--r--app/policies/global_policy.rb2
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/serializers/build_details_entity.rb3
-rw-r--r--app/serializers/deploy_key_entity.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb81
-rw-r--r--app/services/ci/create_trigger_request_service.rb13
-rw-r--r--app/services/ci/pipeline_trigger_service.rb44
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/groups/destroy_service.rb2
-rw-r--r--app/services/issuable_base_service.rb1
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/close_service.rb4
-rw-r--r--app/services/issues/duplicate_service.rb24
-rw-r--r--app/services/issues/update_service.rb18
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/projects/destroy_service.rb72
-rw-r--r--app/services/projects/update_pages_service.rb6
-rw-r--r--app/services/projects/update_service.rb9
-rw-r--r--app/services/quick_actions/interpret_service.rb18
-rw-r--r--app/services/system_note_service.rb38
-rw-r--r--app/services/wiki_pages/update_service.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml15
-rw-r--r--app/views/admin/applications/_form.html.haml8
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml6
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml33
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml16
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml4
-rw-r--r--app/views/groups/_shared_projects.html.haml1
-rw-r--r--app/views/layouts/header/_new.html.haml6
-rw-r--r--app/views/layouts/help.html.haml1
-rw-r--r--app/views/layouts/nav/_new_profile_sidebar.html.haml4
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml6
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/notify/new_gpg_key_email.html.haml10
-rw-r--r--app/views/notify/new_gpg_key_email.text.erb7
-rw-r--r--app/views/profiles/gpg_keys/_email_with_badge.html.haml8
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml10
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml18
-rw-r--r--app/views/profiles/gpg_keys/_key_table.html.haml11
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml21
-rw-r--r--app/views/projects/_deletion_failed.html.haml6
-rw-r--r--app/views/projects/_flash_messages.html.haml8
-rw-r--r--app/views/projects/blame/show.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml3
-rw-r--r--app/views/projects/commit/_commit_box.html.haml1
-rw-r--r--app/views/projects/commit/_invalid_signature_badge.html.haml9
-rw-r--r--app/views/projects/commit/_signature.html.haml5
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml18
-rw-r--r--app/views/projects/commit/_valid_signature_badge.html.haml32
-rw-r--r--app/views/projects/commits/_commit.html.haml10
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml14
-rw-r--r--app/views/projects/empty.html.haml6
-rw-r--r--app/views/projects/labels/index.html.haml13
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml14
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml9
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml7
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml15
-rw-r--r--app/views/projects/milestones/index.html.haml6
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml19
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml6
-rw-r--r--app/views/shared/_clone_panel.html.haml8
-rw-r--r--app/views/shared/_label.html.haml8
-rw-r--r--app/views/shared/_logo_type.svg1
-rw-r--r--app/views/shared/_mr_head.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml15
-rw-r--r--app/views/shared/icons/_icon_clone.svg3
-rw-r--r--app/views/shared/icons/_icon_status_notfound_borderless.svg1
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml10
-rw-r--r--app/views/shared/issuable/_participants.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml9
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml15
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml3
-rw-r--r--app/views/snippets/_snippets.html.haml6
-rw-r--r--app/views/users/calendar.html.haml9
-rw-r--r--app/views/users/show.html.haml12
-rw-r--r--app/workers/create_gpg_signature_worker.rb16
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb12
-rw-r--r--app/workers/pipeline_schedule_worker.rb13
-rw-r--r--app/workers/project_destroy_worker.rb9
212 files changed, 2254 insertions, 1050 deletions
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index 36bfe457be9..510bedbf641 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
+import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index ba9d9a3e1f7..54257531284 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import './lib/utils/common_utils';
+import { placeholderImage } from './lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
@@ -56,6 +57,11 @@ const gfmRules = {
return text;
},
},
+ ImageLazyLoadFilter: {
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ },
VideoLinkFilter: {
'.video-container'(el) {
const videoEl = el.querySelector('video');
@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'img'(el) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ const imageSrc = el.src;
+ const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
+ return `![${el.getAttribute('alt')}](${imageUrl})`;
},
'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 58bd223754f..e625bf24a98 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -20,6 +20,8 @@
/* global NamespaceSelects */
/* global Project */
/* global ProjectAvatar */
+/* global MergeRequest */
+/* global Compare */
/* global CompareAutocomplete */
/* global ProjectNew */
/* global ProjectShow */
@@ -41,7 +43,6 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
-import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
@@ -63,6 +64,10 @@ import ScrollHelper from './helpers/scroll_helper';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
+import initNotes from './init_notes';
+import initLegacyFilters from './init_legacy_filters';
+import initIssuableSidebar from './init_issuable_sidebar';
+import GpgBadges from './gpg_badges';
(function() {
var Dispatcher;
@@ -160,6 +165,8 @@ import PerformanceBar from './performance_bar';
new Issue();
shortcut_handler = new ShortcutsIssuable();
new ZenMode();
+ initIssuableSidebar();
+ initNotes();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
@@ -170,10 +177,12 @@ import PerformanceBar from './performance_bar';
new Milestone();
new Sidebar();
break;
+ case 'dashboard:issues':
+ case 'dashboard:merge_requests':
case 'groups:issues':
case 'groups:merge_requests':
- new UsersSelect();
new ProjectSelect();
+ initLegacyFilters();
break;
case 'dashboard:todos:index':
new Todos();
@@ -225,6 +234,19 @@ import PerformanceBar from './performance_bar';
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:creations:new':
+ const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
+ if (mrNewCompareNode) {
+ new Compare({
+ targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
+ sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
+ targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
+ });
+ } else {
+ const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
+ new MergeRequest({
+ action: mrNewSubmitNode.dataset.mrSubmitAction,
+ });
+ }
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
@@ -241,6 +263,9 @@ import PerformanceBar from './performance_bar';
new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs);
break;
+ case 'projects:snippets:show':
+ initNotes();
+ break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
@@ -261,15 +286,18 @@ import PerformanceBar from './performance_bar';
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
+
+ initIssuableSidebar();
+ initNotes();
+
+ const mrShowNode = document.querySelector('.merge-request');
+ window.mergeRequest = new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
break;
case 'dashboard:activity':
new gl.Activities();
break;
- case 'dashboard:issues':
- case 'dashboard:merge_requests':
- new ProjectSelect();
- new UsersSelect();
- break;
case 'projects:commit:show':
new Commit();
new gl.Diff();
@@ -278,6 +306,7 @@ import PerformanceBar from './performance_bar';
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
+ initNotes();
break;
case 'projects:commit:pipelines':
new MiniPipelineGraph({
@@ -285,6 +314,9 @@ import PerformanceBar from './performance_bar';
}).bindEvents();
break;
case 'projects:commits:show':
+ shortcut_handler = new ShortcutsNavigation();
+ GpgBadges.fetch();
+ break;
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
@@ -356,10 +388,20 @@ import PerformanceBar from './performance_bar';
case 'projects:labels:edit':
new Labels();
break;
+ case 'groups:labels:index':
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
new gl.LabelManager();
}
+ $('.label-subscription').each((i, el) => {
+ const $el = $(el);
+
+ if ($el.find('.dropdown-group-label').length) {
+ new gl.GroupLabelSubscription($el);
+ } else {
+ new gl.ProjectLabelSubscription($el);
+ }
+ });
break;
case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
@@ -384,12 +426,6 @@ import PerformanceBar from './performance_bar';
new Search();
break;
case 'projects:settings:repository:show':
- // Initialize Protected Branch Settings
- new gl.ProtectedBranchCreate();
- new gl.ProtectedBranchEditList();
- // Initialize Protected Tag Settings
- new ProtectedTagCreate();
- new ProtectedTagEditList();
// Initialize expandable settings panels
initSettingsPanels();
break;
@@ -410,10 +446,15 @@ import PerformanceBar from './performance_bar';
case 'snippets:show':
new LineHighlighter();
new BlobViewer();
+ initNotes();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
break;
+ case 'profiles:personal_access_tokens:index':
+ case 'admin:impersonation_tokens:index':
+ new gl.DueDateSelectors();
+ break;
}
switch (path.first()) {
case 'sessions':
@@ -510,6 +551,13 @@ import PerformanceBar from './performance_bar';
case 'protected_branches':
shortcut_handler = new ShortcutsNavigation();
}
+ break;
+ case 'users':
+ const action = path[1];
+ import(/* webpackChunkName: 'user_profile' */ './users')
+ .then(user => user.default(action))
+ .catch(() => {});
+ break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 5838b1bdbb7..a81389ab088 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -2,8 +2,9 @@ import Filter from '~/droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownHint extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, tokenKeys, filter) {
- super(droplab, dropdown, input, filter);
+ constructor(options = {}) {
+ const { input, tokenKeys } = options;
+ super(options);
this.config = {
Filter: {
template: 'hint',
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 34a9e34070c..2615d626c4c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -5,8 +5,9 @@ import Filter from '~/droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, tokenKeys, filter, endpoint, symbol) {
- super(droplab, dropdown, input, filter);
+ constructor(options = {}) {
+ const { input, endpoint, symbol } = options;
+ super(options);
this.symbol = symbol;
this.config = {
Ajax: {
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 19fed771197..7246ccbb281 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -5,8 +5,9 @@ import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class DropdownUser extends gl.FilteredSearchDropdown {
- constructor(droplab, dropdown, input, tokenKeys, filter) {
- super(droplab, dropdown, input, filter);
+ constructor(options = {}) {
+ const { tokenKeys } = options;
+ super(options);
this.config = {
AjaxFilter: {
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 4209ca0d6e2..9e9a9ef74be 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -1,7 +1,7 @@
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
- constructor(droplab, dropdown, input, filter) {
+ constructor({ droplab, dropdown, input, filter }) {
this.droplab = droplab;
this.hookId = input && input.id;
this.input = input;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 6bc6bc43f51..61cef435209 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -42,13 +42,19 @@ class FilteredSearchDropdownManager {
milestone: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
+ extraArguments: {
+ endpoint: `${this.baseEndpoint}/milestones.json`,
+ symbol: '%',
+ },
element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
+ extraArguments: {
+ endpoint: `${this.baseEndpoint}/labels.json`,
+ symbol: '~',
+ },
element: this.container.querySelector('#js-dropdown-label'),
},
hint: {
@@ -97,13 +103,19 @@ class FilteredSearchDropdownManager {
let forceShowList = false;
if (!mappingKey.reference) {
- const dl = this.droplab;
- const defaultArguments =
- [null, dl, element, this.filteredSearchInput, this.filteredSearchTokenKeys, key];
- const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+ const defaultArguments = {
+ droplab: this.droplab,
+ dropdown: element,
+ input: this.filteredSearchInput,
+ tokenKeys: this.filteredSearchTokenKeys,
+ filter: key,
+ };
+ const extraArguments = mappingKey.extraArguments || {};
+ const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new gl[glClass](<arguments>)`
- mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
+ mappingKey.reference =
+ new (Function.prototype.bind.apply(gl[glClass], [null, glArguments]))();
}
if (firstLoad) {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
new file mode 100644
index 00000000000..1c379e9bb67
--- /dev/null
+++ b/app/assets/javascripts/gpg_badges.js
@@ -0,0 +1,15 @@
+export default class GpgBadges {
+ static fetch() {
+ const form = $('.commits-search-form');
+
+ $.get({
+ url: form.data('signatures-path'),
+ data: form.serialize(),
+ }).done((response) => {
+ const badges = $('.js-loading-gpg-badge');
+ response.signatures.forEach((signature) => {
+ badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js
new file mode 100644
index 00000000000..19f4a946f73
--- /dev/null
+++ b/app/assets/javascripts/how_to_merge.js
@@ -0,0 +1,12 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const modal = $('#modal_merge_info').modal({
+ modal: true,
+ show: false,
+ });
+ $('.how_to_merge_link').on('click', () => {
+ modal.show();
+ });
+ $('.modal-header .close').on('click', () => {
+ modal.hide();
+ });
+});
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
new file mode 100644
index 00000000000..29e3d2ea94e
--- /dev/null
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -0,0 +1,18 @@
+/* eslint-disable no-new */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global IssuableContext */
+/* global Sidebar */
+
+export default () => {
+ const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+
+ new MilestoneSelect({
+ full_path: sidebarOptions.fullPath,
+ });
+ new LabelsSelect();
+ new IssuableContext(sidebarOptions.currentUser);
+ gl.Subscription.bindAll('.subscription');
+ new gl.DueDateSelectors();
+ window.sidebar = new Sidebar();
+};
diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js
new file mode 100644
index 00000000000..1211c2c802c
--- /dev/null
+++ b/app/assets/javascripts/init_legacy_filters.js
@@ -0,0 +1,15 @@
+/* eslint-disable no-new */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global IssueStatusSelect */
+/* global SubscriptionSelect */
+
+import UsersSelect from './users_select';
+
+export default () => {
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
+};
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
new file mode 100644
index 00000000000..3a8b4360cb6
--- /dev/null
+++ b/app/assets/javascripts/init_notes.js
@@ -0,0 +1,14 @@
+/* global Notes */
+
+export default () => {
+ const dataEl = document.querySelector('.js-notes-data');
+ const {
+ notesUrl,
+ notesIds,
+ now,
+ diffView,
+ autocomplete,
+ } = JSON.parse(dataEl.innerHTML);
+
+ window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete);
+};
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index ddd3a6aab99..cf1e6a14725 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
- new Flash(res.message, null, null, {
+ new Flash(`${res.message} ${res.service_response}`, null, null, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 4f376599ba9..d314f3c4d43 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -86,10 +86,23 @@ export default class IssuableBulkUpdateSidebar {
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() {
this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
}
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index a4d7bf096ef..26392db4b5b 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -4,6 +4,8 @@
import Cookies from 'js-cookie';
import UsersSelect from './users_select';
+const PARTICIPANTS_ROW_COUNT = 7;
+
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
@@ -50,11 +52,9 @@ import UsersSelect from './users_select';
}
IssuableContext.prototype.initParticipants = function() {
- var _this;
- _this = this;
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
return $(".js-participants-author").each(function(i) {
- if (i >= _this.PARTICIPANTS_ROW_COUNT) {
+ if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass("js-participants-hidden").hide();
}
});
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
new file mode 100644
index 00000000000..3d64b121fa7
--- /dev/null
+++ b/app/assets/javascripts/lazy_loader.js
@@ -0,0 +1,76 @@
+/* eslint-disable one-export, one-var, one-var-declaration-per-line */
+
+import _ from 'underscore';
+
+export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+const SCROLL_THRESHOLD = 300;
+
+export default class LazyLoader {
+ constructor(options = {}) {
+ this.lazyImages = [];
+ this.observerNode = options.observerNode || '#content-body';
+
+ const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
+ const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
+
+ window.addEventListener('scroll', throttledScrollCheck);
+ window.addEventListener('resize', debouncedElementsInView);
+
+ const scrollContainer = options.scrollContainer || window;
+ scrollContainer.addEventListener('load', () => this.loadCheck());
+ }
+ searchLazyImages() {
+ this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
+ this.checkElementsInView();
+ }
+ startContentObserver() {
+ const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
+
+ if (contentNode) {
+ const observer = new MutationObserver(() => this.searchLazyImages());
+
+ observer.observe(contentNode, {
+ childList: true,
+ subtree: true,
+ });
+ }
+ }
+ loadCheck() {
+ this.searchLazyImages();
+ this.startContentObserver();
+ }
+ scrollCheck() {
+ requestAnimationFrame(() => this.checkElementsInView());
+ }
+ checkElementsInView() {
+ const scrollTop = pageYOffset;
+ const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
+ let imgBoundRect, imgTop, imgBound;
+
+ // Loading Images which are in the current viewport or close to them
+ this.lazyImages = this.lazyImages.filter((selectedImage) => {
+ if (selectedImage.getAttribute('data-src')) {
+ imgBoundRect = selectedImage.getBoundingClientRect();
+
+ imgTop = scrollTop + imgBoundRect.top;
+ imgBound = imgTop + imgBoundRect.height;
+
+ if (scrollTop < imgBound && visHeight > imgTop) {
+ LazyLoader.loadImage(selectedImage);
+ return false;
+ }
+
+ return true;
+ }
+ return false;
+ });
+ }
+ static loadImage(img) {
+ if (img.getAttribute('data-src')) {
+ img.setAttribute('src', img.getAttribute('data-src'));
+ img.removeAttribute('data-src');
+ img.classList.remove('lazy');
+ img.classList.add('js-lazy-loaded');
+ }
+ }
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 26c67fb721c..cd45091c211 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -109,6 +109,7 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
+import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
@@ -144,7 +145,6 @@ import './right_sidebar';
import './search';
import './search_autocomplete';
import './smart_interval';
-import './snippets_list';
import './star';
import './subscription';
import './subscription_select';
@@ -158,6 +158,8 @@ document.addEventListener('beforeunload', function () {
$(document).off('scroll');
// Close any open tooltips
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ // Close any open popover
+ $('[data-toggle="popover"]').popover('destroy');
});
window.addEventListener('hashchange', gl.utils.handleLocationHash);
@@ -166,6 +168,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash();
}, false);
+gl.lazyLoader = new LazyLoader({
+ scrollContainer: window,
+ observerNode: '#content-body'
+});
+
$(function () {
var $body = $('body');
var $document = $(document);
@@ -241,6 +248,11 @@ $(function () {
return $(el).data('placement') || 'bottom';
}
});
+ // Initialize popovers
+ $body.popover({
+ selector: '[data-toggle="popover"]',
+ trigger: 'focus'
+ });
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
// Form submitter
@@ -284,13 +296,7 @@ $(function () {
return $container.remove();
// Commit show suppressed diff
});
- $('.navbar-toggle').on('click', function () {
- $('.header-content .title, .header-content .navbar-sub-nav').toggle();
- $('.header-content .header-logo').toggle();
- $('.header-content .navbar-collapse').toggle();
- $('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
- return $('.navbar-toggle').toggleClass('active');
- });
+ $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded'));
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
@@ -347,4 +353,14 @@ $(function () {
gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs');
+
+ $('form.filter-form').on('submit', function (event) {
+ const link = document.createElement('a');
+ link.href = this.action;
+
+ const action = `${this.action}${link.search === '' ? '?' : '&'}`;
+
+ event.preventDefault();
+ gl.utils.visitUrl(`${action}${$(this).serialize()}`);
+ });
});
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index c4e379a4a0b..8be7314ded8 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -175,7 +175,7 @@ import Cookies from 'js-cookie';
getConflictsCountText() {
const count = this.getConflictsCount();
- const text = count ? 'conflicts' : 'conflict';
+ const text = count > 1 ? 'conflicts' : 'conflict';
return `${count} ${text}`;
},
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 17030c3e4d3..d74cf5328ad 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -2,6 +2,7 @@
/* global Flash */
import Vue from 'vue';
+import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
import './mixins/line_conflict_utils';
@@ -19,6 +20,8 @@ $(() => {
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath
});
+ initIssuableSidebar();
+
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
data: mergeConflictsStore.state,
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 9d481d7c003..6756ab0b3aa 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -8,7 +8,7 @@
var _this, $els;
if (currentProject != null) {
_this = this;
- this.currentProject = JSON.parse(currentProject);
+ this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
$els = $(els);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 738e710deb9..a3f7d69b98d 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -6,21 +6,22 @@ import Cookies from 'js-cookie';
(function() {
this.Project = (function() {
function Project() {
- $('ul.clone-options-dropdown a').click(function() {
- var url;
- if ($(this).hasClass('active')) {
- return;
- }
- $('.active').not($(this)).removeClass('active');
- $(this).toggleClass('active');
- url = $("#project_clone").val();
- $('#project_clone').val(url);
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
+
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+
+ e.preventDefault();
+
+ $('.active', $cloneOptions).not($this).removeClass('active');
+ $this.toggleClass('active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text($this.text());
+
return $('.clone').text(url);
- // Git protocol switcher
- // Remove the active class for all buttons (ssh, http, kerberos if shown)
- // Add the active class for the clicked button
- // Update the input field
- // Update the command line instructions
});
// Ref switcher
this.initRefSwitcher();
diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js
new file mode 100644
index 00000000000..c9e7af127d2
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/index.js
@@ -0,0 +1,9 @@
+/* eslint-disable no-unused-vars */
+
+import ProtectedBranchCreate from './protected_branch_create';
+import ProtectedBranchEditList from './protected_branch_edit_list';
+
+$(() => {
+ const protectedBranchCreate = new ProtectedBranchCreate();
+ const protectedBranchEditList = new ProtectedBranchEditList();
+});
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
index 42993a252c3..38b1406a99f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
@@ -1,31 +1,26 @@
-/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */
+export default class ProtectedBranchAccessDropdown {
+ constructor(options) {
+ this.options = options;
+ this.initDropdown();
+ }
-(global => {
- global.gl = global.gl || {};
-
- gl.ProtectedBranchAccessDropdown = class {
- constructor(options) {
- const { $dropdown, data, onSelect } = options;
-
- $dropdown.glDropdown({
- data: data,
- selectable: true,
- inputId: $dropdown.data('input-id'),
- fieldName: $dropdown.data('field-name'),
- toggleLabel(item, el) {
- if (el.is('.is-active')) {
- return item.text;
- } else {
- return 'Select';
- }
- },
- clicked(opts) {
- const { e } = opts;
-
- e.preventDefault();
- onSelect();
+ initDropdown() {
+ const { $dropdown, data, onSelect } = this.options;
+ $dropdown.glDropdown({
+ data,
+ selectable: true,
+ inputId: $dropdown.data('input-id'),
+ fieldName: $dropdown.data('field-name'),
+ toggleLabel(item, $el) {
+ if ($el.is('.is-active')) {
+ return item.text;
}
- });
- }
- };
-})(window);
+ return 'Select';
+ },
+ clicked(options) {
+ options.e.preventDefault();
+ onSelect();
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 57ea2f52814..10da3783123 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,55 +1,51 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
-/* global ProtectedBranchDropdown */
-
-(global => {
- global.gl = global.gl || {};
-
- gl.ProtectedBranchCreate = class {
- constructor() {
- this.$wrap = this.$form = $('#new_protected_branch');
- this.buildDropdowns();
- }
-
- buildDropdowns() {
- const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
- const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
-
- // Cache callback
- this.onSelectCallback = this.onSelect.bind(this);
-
- // Allowed to Merge dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: $allowedToMergeDropdown,
- data: gon.merge_access_levels,
- onSelect: this.onSelectCallback
- });
-
- // Allowed to Push dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: $allowedToPushDropdown,
- data: gon.push_access_levels,
- onSelect: this.onSelectCallback
- });
-
- // Select default
- $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
- $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
-
- // Protected branch dropdown
- new ProtectedBranchDropdown({
- $dropdown: this.$wrap.find('.js-protected-branch-select'),
- onSelect: this.onSelectCallback
- });
- }
-
- // This will run after clicked callback
- onSelect() {
- // Enable submit button
- const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
- const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
-
- this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
- }
- };
-})(window);
+import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
+import ProtectedBranchDropdown from './protected_branch_dropdown';
+
+export default class ProtectedBranchCreate {
+ constructor() {
+ this.$form = $('.js-new-protected-branch');
+ this.buildDropdowns();
+ }
+
+ buildDropdowns() {
+ const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
+ const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
+
+ // Cache callback
+ this.onSelectCallback = this.onSelect.bind(this);
+
+ // Allowed to Merge dropdown
+ this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({
+ $dropdown: $allowedToMergeDropdown,
+ data: gon.merge_access_levels,
+ onSelect: this.onSelectCallback,
+ });
+
+ // Allowed to Push dropdown
+ this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({
+ $dropdown: $allowedToPushDropdown,
+ data: gon.push_access_levels,
+ onSelect: this.onSelectCallback,
+ });
+
+ // Select default
+ $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
+ $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
+
+ // Protected branch dropdown
+ this.protectedBranchDropdown = new ProtectedBranchDropdown({
+ $dropdown: this.$form.find('.js-protected-branch-select'),
+ onSelect: this.onSelectCallback,
+ });
+ }
+
+ // This will run after clicked callback
+ onSelect() {
+ // Enable submit button
+ const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
+ const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
+ const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+
+ this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index bc6110fcd4e..cc0b2ebe071 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -1,6 +1,10 @@
-/* eslint-disable comma-dangle, no-unused-vars */
-
-class ProtectedBranchDropdown {
+export default class ProtectedBranchDropdown {
+ /**
+ * @param {Object} options containing
+ * `$dropdown` target element
+ * `onSelect` event callback
+ * $dropdown must be an element created using `dropdown_branch()` rails helper
+ */
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
@@ -12,7 +16,7 @@ class ProtectedBranchDropdown {
this.bindEvents();
// Hide footer
- this.$dropdownFooter.addClass('hidden');
+ this.toggleFooter(true);
}
buildDropdown() {
@@ -21,7 +25,7 @@ class ProtectedBranchDropdown {
filterable: true,
remote: false,
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
toggleLabel(selected) {
@@ -36,10 +40,9 @@ class ProtectedBranchDropdown {
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
- const { $el, e } = options;
- e.preventDefault();
+ options.e.preventDefault();
this.onSelect();
- }
+ },
});
}
@@ -64,20 +67,22 @@ class ProtectedBranchDropdown {
}
toggleCreateNewButton(branchName) {
- this.selectedBranch = {
- title: branchName,
- id: branchName,
- text: branchName
- };
-
if (branchName) {
+ this.selectedBranch = {
+ title: branchName,
+ id: branchName,
+ text: branchName,
+ };
+
this.$dropdownContainer
.find('.js-create-new-protected-branch code')
.text(branchName);
}
- this.$dropdownFooter.toggleClass('hidden', !branchName);
+ this.toggleFooter(!branchName);
}
-}
-window.ProtectedBranchDropdown = ProtectedBranchDropdown;
+ toggleFooter(toggleState) {
+ this.$dropdownFooter.toggleClass('hidden', toggleState);
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 6ef59e94384..3b920942a3f 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,69 +1,67 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
+/* eslint-disable no-new */
/* global Flash */
-(global => {
- global.gl = global.gl || {};
+import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
- gl.ProtectedBranchEdit = class {
- constructor(options) {
- this.$wrap = options.$wrap;
- this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
- this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+export default class ProtectedBranchEdit {
+ constructor(options) {
+ this.$wrap = options.$wrap;
+ this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+ this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+ this.onSelectCallback = this.onSelect.bind(this);
- this.buildDropdowns();
- }
+ this.buildDropdowns();
+ }
- buildDropdowns() {
- // Allowed to merge dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: this.$allowedToMergeDropdown,
- data: gon.merge_access_levels,
- onSelect: this.onSelect.bind(this)
- });
+ buildDropdowns() {
+ // Allowed to merge dropdown
+ this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToMergeDropdown,
+ data: gon.merge_access_levels,
+ onSelect: this.onSelectCallback,
+ });
- // Allowed to push dropdown
- new gl.ProtectedBranchAccessDropdown({
- $dropdown: this.$allowedToPushDropdown,
- data: gon.push_access_levels,
- onSelect: this.onSelect.bind(this)
- });
- }
+ // Allowed to push dropdown
+ this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToPushDropdown,
+ data: gon.push_access_levels,
+ onSelect: this.onSelectCallback,
+ });
+ }
- onSelect() {
- const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
- const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+ onSelect() {
+ const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
+ const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
- // Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ // Do not update if one dropdown has not selected any option
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
- this.$allowedToMergeDropdown.disable();
- this.$allowedToPushDropdown.disable();
+ this.$allowedToMergeDropdown.disable();
+ this.$allowedToPushDropdown.disable();
- $.ajax({
- type: 'POST',
- url: this.$wrap.data('url'),
- dataType: 'json',
- data: {
- _method: 'PATCH',
- protected_branch: {
- merge_access_levels_attributes: [{
- id: this.$allowedToMergeDropdown.data('access-level-id'),
- access_level: $allowedToMergeInput.val()
- }],
- push_access_levels_attributes: [{
- id: this.$allowedToPushDropdown.data('access-level-id'),
- access_level: $allowedToPushInput.val()
- }]
- }
+ $.ajax({
+ type: 'POST',
+ url: this.$wrap.data('url'),
+ dataType: 'json',
+ data: {
+ _method: 'PATCH',
+ protected_branch: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
+ access_level: $allowedToMergeInput.val(),
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
+ access_level: $allowedToPushInput.val(),
+ }],
},
- error() {
- $.scrollTo(0);
- new Flash('Failed to update branch!');
- }
- }).always(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
- });
- }
- };
-})(window);
+ },
+ error() {
+ new Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
+ },
+ }).always(() => {
+ this.$allowedToMergeDropdown.enable();
+ this.$allowedToPushDropdown.enable();
+ });
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
index 336fa6c57a7..b40d3827c30 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
@@ -1,18 +1,18 @@
-/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
+/* eslint-disable no-new */
-(global => {
- global.gl = global.gl || {};
+import ProtectedBranchEdit from './protected_branch_edit';
- gl.ProtectedBranchEditList = class {
- constructor() {
- this.$wrap = $('.protected-branches-list');
+export default class ProtectedBranchEditList {
+ constructor() {
+ this.$wrap = $('.protected-branches-list');
+ this.initEditForm();
+ }
- // Build edit forms
- this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
- new gl.ProtectedBranchEdit({
- $wrap: $(el)
- });
+ initEditForm() {
+ this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
+ new ProtectedBranchEdit({
+ $wrap: $(el),
});
- }
- };
-})(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
deleted file mode 100644
index 874d70a1431..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import './protected_branch_access_dropdown';
-import './protected_branch_create';
-import './protected_branch_dropdown';
-import './protected_branch_edit';
-import './protected_branch_edit_list';
diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
index 61e7ba53862..b1618e24e49 100644
--- a/app/assets/javascripts/protected_tags/index.js
+++ b/app/assets/javascripts/protected_tags/index.js
@@ -1,2 +1,9 @@
-export { default as ProtectedTagCreate } from './protected_tag_create';
-export { default as ProtectedTagEditList } from './protected_tag_edit_list';
+/* eslint-disable no-unused-vars */
+
+import ProtectedTagCreate from './protected_tag_create';
+import ProtectedTagEditList from './protected_tag_edit_list';
+
+$(() => {
+ const protectedtTagCreate = new ProtectedTagCreate();
+ const protectedtTagEditList = new ProtectedTagEditList();
+});
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 2b02af87d8a..a9df66748c5 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -5,7 +5,8 @@ import sidebarAssignees from './components/assignees/sidebar_assignees';
import Mediator from './sidebar_mediator';
function domContentLoaded() {
- const mediator = new Mediator(gl.sidebarOptions);
+ const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+ const mediator = new Mediator(sidebarOptions);
mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
diff --git a/app/assets/javascripts/snippets_list.js b/app/assets/javascripts/snippets_list.js
deleted file mode 100644
index 3b6d999b1c3..00000000000
--- a/app/assets/javascripts/snippets_list.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function SnippetsList() {
- const $holder = $('.snippets-list-holder');
-
- $holder.find('.pagination').on('ajax:success', (e, data) => {
- $holder.replaceWith(data.html);
- });
-}
-
-window.gl.SnippetsList = SnippetsList;
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 6d38124f1c1..3a06b477d7c 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,6 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
+import { __, s__ } from './locale';
+
export default class Star {
constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
@@ -11,10 +13,10 @@ export default class Star {
toggleStar = function(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
- $starSpan.removeClass('starred').text('Star');
+ $starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
- $starSpan.addClass('starred').text('Unstar');
+ $starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
};
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index cd305631c10..bba8b5abbb4 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -37,10 +37,6 @@ export default class Todos {
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
- $('form.filter-form').on('submit', function applyFilters(event) {
- event.preventDefault();
- gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
- });
return new UsersSelect();
}
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index b7f50cfd083..f091e319f44 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,10 +1,28 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len, class-methods-use-this */
-
import d3 from 'd3';
+const LOADING_HTML = `
+ <div class="text-center">
+ <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
+ </div>
+`;
+
+function formatTooltipText({ date, count }) {
+ const dateObject = new Date(date);
+ const dateDayName = gl.utils.getDayName(dateObject);
+ const dateText = dateObject.format('mmm d, yyyy');
+
+ let contribText = 'No contributions';
+ if (count > 0) {
+ contribText = `${count} contribution${count > 1 ? 's' : ''}`;
+ }
+ return `${contribText}<br />${dateDayName} ${dateText}`;
+}
+
+const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+
export default class ActivityCalendar {
- constructor(timestamps, calendar_activities_path) {
- this.calendar_activities_path = calendar_activities_path;
+ constructor(container, timestamps, calendarActivitiesPath) {
+ this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
@@ -12,25 +30,26 @@ export default class ActivityCalendar {
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
+
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
- var group = 0;
+ let group = 0;
- var today = new Date();
+ const today = new Date();
today.setHours(0, 0, 0, 0, 0);
- var oneYearAgo = new Date(today);
+ const oneYearAgo = new Date(today);
oneYearAgo.setFullYear(today.getFullYear() - 1);
- var days = gl.utils.getDayDifference(oneYearAgo, today);
+ const days = gl.utils.getDayDifference(oneYearAgo, today);
- for (var i = 0; i <= days; i += 1) {
- var date = new Date(oneYearAgo);
+ for (let i = 0; i <= days; i += 1) {
+ const date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
- var day = date.getDay();
- var count = timestamps[date.format('yyyy-mm-dd')];
+ const day = date.getDay();
+ const count = timestamps[date.format('yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
@@ -39,129 +58,119 @@ export default class ActivityCalendar {
group += 1;
}
- var innerArray = this.timestampsTmp[group - 1];
// Push to the inner array the values that will be used to render map
- innerArray.push({
- count: count || 0,
- date: date,
- day: day
- });
+ const innerArray = this.timestampsTmp[group - 1];
+ innerArray.push({ count, date, day });
}
// Init color functions
- this.colorKey = this.initColorKey();
+ this.colorKey = initColorKey();
this.color = this.initColor();
+
// Init the svg element
- this.renderSvg(group);
+ this.svg = this.renderSvg(container, group);
this.renderDays();
this.renderMonths();
this.renderDayTitles();
this.renderKey();
- this.initTooltips();
+
+ // Init tooltips
+ $(`${container} .js-tooltip`).tooltip({ html: true });
}
// Add extra padding for the last month label if it is also the last column
getExtraWidthPadding(group) {
- var extraWidthPadding = 0;
- var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
- var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
+ let extraWidthPadding = 0;
+ const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
+ const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
- if (lastColMonth != secondLastColMonth) {
+ if (lastColMonth !== secondLastColMonth) {
extraWidthPadding = 3;
}
return extraWidthPadding;
}
- renderSvg(group) {
- var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
- return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
+ renderSvg(container, group) {
+ const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
+ return d3.select(container)
+ .append('svg')
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
}
renderDays() {
- return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) {
- return function(group, i) {
- _.each(group, function(stamp, a) {
- var lastMonth, lastMonthX, month, x;
+ this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ .attr('transform', (group, i) => {
+ _.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
- month = stamp.date.getMonth();
- x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace;
- lastMonth = _.last(_this.months);
- if (lastMonth != null) {
- lastMonthX = lastMonth.x;
- }
- if (lastMonth == null) {
- return _this.months.push({
- month: month,
- x: x
- });
- } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) {
- return _this.months.push({
- month: month,
- x: x
- });
+ const month = stamp.date.getMonth();
+ const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const lastMonth = _.last(this.months);
+ if (
+ lastMonth == null ||
+ (month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x)
+ ) {
+ this.months.push({ month, x });
}
}
});
- return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)";
- };
- })(this)).selectAll('rect').data(function(stamp) {
- return stamp;
- }).enter().append('rect').attr('x', '0').attr('y', (function(_this) {
- return function(stamp, i) {
- return _this.daySizeWithSpace * stamp.day;
- };
- })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) {
- return function(stamp) {
- var contribText, date, dateText;
- date = new Date(stamp.date);
- contribText = 'No contributions';
- if (stamp.count > 0) {
- contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
- }
- dateText = date.format('mmm d, yyyy');
- return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
- };
- })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
- return function(stamp) {
- if (stamp.count !== 0) {
- return _this.color(Math.min(stamp.count, 40));
- } else {
- return '#ededed';
- }
- };
- })(this)).attr('data-container', 'body').on('click', this.clickDay);
+ return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ })
+ .selectAll('rect')
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.daySizeWithSpace * stamp.day)
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('fill', stamp => (
+ stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
+ ))
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
- var days;
- days = [
+ const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1)
+ y: 29 + (this.daySizeWithSpace * 1),
}, {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3)
+ y: 29 + (this.daySizeWithSpace * 3),
}, {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5)
- }
+ y: 29 + (this.daySizeWithSpace * 5),
+ },
];
- return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) {
- return day.y;
- }).text(function(day) {
- return day.text;
- }).attr('class', 'user-contrib-text');
+ this.svg.append('g')
+ .selectAll('text')
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) {
- return date.x;
- }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) {
- return function(date) {
- return _this.monthNames[date.month];
- };
- })(this));
+ this.svg.append('g')
+ .attr('direction', 'ltr')
+ .selectAll('text')
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
@@ -169,7 +178,7 @@ export default class ActivityCalendar {
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
this.svg.append('g')
- .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
+ .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
@@ -185,43 +194,31 @@ export default class ActivityCalendar {
}
initColor() {
- var colorRange;
- colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
}
- initColorKey() {
- return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
- }
-
clickDay(stamp) {
- var formatted_date;
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;
- formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate();
- return $.ajax({
- url: this.calendar_activities_path,
- data: {
- date: formatted_date
- },
+
+ const date = [
+ this.currentSelectedDate.getFullYear(),
+ this.currentSelectedDate.getMonth() + 1,
+ this.currentSelectedDate.getDate(),
+ ].join('-');
+
+ $.ajax({
+ url: this.calendarActivitiesPath,
+ data: { date },
cache: false,
dataType: 'html',
- beforeSend: function() {
- return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>');
- },
- success: function(data) {
- return $('.user-calendar-activities').html(data);
- }
+ beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML),
+ success: data => $('.user-calendar-activities').html(data),
});
} else {
this.currentSelectedDate = '';
- return $('.user-calendar-activities').html('');
+ $('.user-calendar-activities').html('');
}
}
-
- initTooltips() {
- return $('.js-contrib-calendar .js-tooltip').tooltip({
- html: true
- });
- }
}
diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js
index ecd8e09161e..33a83f8dae5 100644
--- a/app/assets/javascripts/users/index.js
+++ b/app/assets/javascripts/users/index.js
@@ -1,7 +1,19 @@
-import ActivityCalendar from './activity_calendar';
-import User from './user';
+import Cookies from 'js-cookie';
+import UserTabs from './user_tabs';
-// use legacy exports until embedded javascript is refactored
-window.Calendar = ActivityCalendar;
-window.gl = window.gl || {};
-window.gl.User = User;
+export default function initUserProfile(action) {
+ // place profile avatars to top
+ $('.profile-groups-avatars').tooltip({
+ placement: 'top',
+ });
+
+ // eslint-disable-next-line no-new
+ new UserTabs({ parentEl: '.user-profile', action });
+
+ // hide project limit message
+ $('.hide-project-limit-message').on('click', (e) => {
+ e.preventDefault();
+ Cookies.set('hide_project_limit_message', 'false');
+ $(this).parents('.project-limit-message').remove();
+ });
+}
diff --git a/app/assets/javascripts/users/user.js b/app/assets/javascripts/users/user.js
deleted file mode 100644
index 0b0a3e1afb4..00000000000
--- a/app/assets/javascripts/users/user.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable class-methods-use-this */
-
-import Cookies from 'js-cookie';
-import UserTabs from './user_tabs';
-
-export default class User {
- constructor({ action }) {
- this.action = action;
- this.placeProfileAvatarsToTop();
- this.initTabs();
- this.hideProjectLimitMessage();
- }
-
- placeProfileAvatarsToTop() {
- $('.profile-groups-avatars').tooltip({
- placement: 'top',
- });
- }
-
- initTabs() {
- return new UserTabs({
- parentEl: '.user-profile',
- action: this.action,
- });
- }
-
- hideProjectLimitMessage() {
- $('.hide-project-limit-message').on('click', (e) => {
- e.preventDefault();
- Cookies.set('hide_project_limit_message', 'false');
- $(this).parents('.project-limit-message').remove();
- });
- }
-}
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js
index f8e23c8624d..5fe6603ce7b 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/users/user_tabs.js
@@ -1,72 +1,76 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */
-
-/*
-UserTabs
-
-Handles persisting and restoring the current tab selection and lazily-loading
-content on the Users#show page.
-
-### Example Markup
-
- <ul class="nav-links">
- <li class="activity-tab active">
- <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
- Activity
- </a>
- </li>
- <li class="groups-tab">
- <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
- Groups
- </a>
- </li>
- <li class="contributed-tab">
- <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
- Contributed projects
- </a>
- </li>
- <li class="projects-tab">
- <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
- Personal projects
- </a>
- </li>
- <li class="snippets-tab">
- <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
- </a>
- </li>
- </ul>
-
- <div class="tab-content">
- <div class="tab-pane" id="activity">
- Activity Content
- </div>
- <div class="tab-pane" id="groups">
- Groups Content
- </div>
- <div class="tab-pane" id="contributed">
- Contributed projects content
- </div>
- <div class="tab-pane" id="projects">
- Projects content
- </div>
- <div class="tab-pane" id="snippets">
- Snippets content
- </div>
+import ActivityCalendar from './activity_calendar';
+
+/**
+ * UserTabs
+ *
+ * Handles persisting and restoring the current tab selection and lazily-loading
+ * content on the Users#show page.
+ *
+ * ### Example Markup
+ *
+ * <ul class="nav-links">
+ * <li class="activity-tab active">
+ * <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+ * Activity
+ * </a>
+ * </li>
+ * <li class="groups-tab">
+ * <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+ * Groups
+ * </a>
+ * </li>
+ * <li class="contributed-tab">
+ * ...
+ * </li>
+ * <li class="projects-tab">
+ * ...
+ * </li>
+ * <li class="snippets-tab">
+ * ...
+ * </li>
+ * </ul>
+ *
+ * <div class="tab-content">
+ * <div class="tab-pane" id="activity">
+ * Activity Content
+ * </div>
+ * <div class="tab-pane" id="groups">
+ * Groups Content
+ * </div>
+ * <div class="tab-pane" id="contributed">
+ * Contributed projects content
+ * </div>
+ * <div class="tab-pane" id="projects">
+ * Projects content
+ * </div>
+ * <div class="tab-pane" id="snippets">
+ * Snippets content
+ * </div>
+ * </div>
+ *
+ * <div class="loading-status">
+ * <div class="loading">
+ * Loading Animation
+ * </div>
+ * </div>
+ */
+
+const CALENDAR_TEMPLATE = `
+ <div class="clearfix calendar">
+ <div class="js-contrib-calendar"></div>
+ <div class="calendar-hint">
+ Summary of issues, merge requests, push events, and comments
+ </div>
</div>
-
- <div class="loading-status">
- <div class="loading">
- Loading Animation
- </div>
- </div>
-*/
+`;
export default class UserTabs {
- constructor ({ defaultAction, action, parentEl }) {
+ constructor({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
- this._location = window.location;
+ this.windowLocation = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
@@ -82,12 +86,10 @@ export default class UserTabs {
}
bindEvents() {
- this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
-
- this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
- .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
-
- this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
+ this.$parentEl
+ .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event))
+ .on('click', '.gl-pagination a', event => this.changeProjectsPage(event));
}
changeProjectsPage(e) {
@@ -122,7 +124,7 @@ export default class UserTabs {
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
- return this.loadTab(action, endpoint);
+ this.loadTab(action, endpoint);
}
}
@@ -131,25 +133,38 @@ export default class UserTabs {
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
- type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
- return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
- }
+ gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ },
});
}
loadActivities() {
- if (this.loaded['activity']) {
+ if (this.loaded.activity) {
return;
}
const $calendarWrap = this.$parentEl.find('.user-calendar');
- $calendarWrap.load($calendarWrap.data('href'));
+ const calendarPath = $calendarWrap.data('calendarPath');
+ const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
+
+ $.ajax({
+ dataType: 'json',
+ url: calendarPath,
+ success: (activityData) => {
+ $calendarWrap.html(CALENDAR_TEMPLATE);
+
+ // eslint-disable-next-line no-new
+ new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath);
+ },
+ });
+
+ // eslint-disable-next-line no-new
new gl.Activities();
- return this.loaded['activity'] = true;
+ this.loaded.activity = true;
}
toggleLoading(status) {
@@ -158,13 +173,13 @@ export default class UserTabs {
}
setCurrentAction(source) {
- let new_state = source;
- new_state = new_state.replace(/\/+$/, '');
- new_state += this._location.search + this._location.hash;
+ let newState = source;
+ newState = newState.replace(/\/+$/, '');
+ newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({
- url: new_state
- }, document.title, new_state);
- return new_state;
+ url: newState,
+ }, document.title, newState);
+ return newState;
}
getCurrentAction() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index e8e22ad93a5..744a1cd24fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -108,7 +108,8 @@ export default {
</div>
<mr-widget-memory-usage
v-if="deployment.metrics_url"
- :metricsUrl="deployment.metrics_url"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 76cb71b6c12..534e2a88eff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MemoryUsage',
props: {
- metricsUrl: { type: String, required: true },
+ metricsUrl: {
+ type: String,
+ required: true,
+ },
+ metricsMonitoringUrl: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -124,7 +131,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
+ <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 06f7af33f94..0dfa7a31d31 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -35,6 +35,8 @@
width: 40px;
height: 40px;
padding: 0;
+ background: $avatar-background;
+ overflow: hidden;
&.avatar-inline {
float: none;
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 759401a7806..0ac095f7d8f 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -93,7 +93,7 @@
.is-selected .pika-day,
.pika-day:hover,
- .is-today .pika-day:hover {
+ .is-today .pika-day {
background: $gl-primary;
color: $white-light;
box-shadow: none;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5e410cbf563..3f934403147 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -148,7 +148,6 @@
padding: 5px 8px;
color: $gl-text-color;
line-height: initial;
- text-overflow: ellipsis;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
@@ -203,11 +202,6 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
- @media (max-width: $screen-sm-min) {
- width: 100%;
- min-width: 180px;
- }
-
&.dropdown-open-left {
right: 0;
left: auto;
@@ -289,6 +283,11 @@
padding: 5px 8px;
color: $gl-text-color-secondary;
}
+
+ .badge + span:not(.badge) {
+ // Expects up to 3 digits on the badge
+ margin-right: 40px;
+ }
}
.droplab-dropdown {
@@ -373,7 +372,6 @@
.dropdown-menu,
.dropdown-menu-nav {
max-width: 280px;
- width: auto;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c7c2684d548..8ad082f7a65 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -163,8 +163,18 @@
td.blame-commit {
padding: 5px 10px;
min-width: 400px;
+ max-width: 400px;
background: $gray-light;
border-left: 3px solid;
+
+ .commit-row-title {
+ display: flex;
+ }
+
+ .item-title {
+ flex: 1;
+ margin-right: 0.5em;
+ }
}
@for $i from 0 through 5 {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 41184907abb..ab2abaca33a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -393,7 +393,8 @@
@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
- .dropdown {
+ .dropdown,
+ .dropdown-menu {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 20fb10c28d4..605f4284bb5 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -132,6 +132,22 @@ header {
}
}
+ &.navbar-gitlab-new {
+ .fa-times {
+ display: none;
+ }
+
+ .menu-expanded {
+ .fa-ellipsis-v {
+ display: none;
+ }
+
+ .fa-times {
+ display: block;
+ }
+ }
+ }
+
.global-dropdown {
position: absolute;
left: -10px;
@@ -171,6 +187,19 @@ header {
min-height: $header-height;
padding-left: 30px;
+ &.menu-expanded {
+ @media (max-width: $screen-xs-max) {
+ .header-logo,
+ .title-container {
+ display: none;
+ }
+
+ .navbar-collapse {
+ display: block;
+ }
+ }
+ }
+
.dropdown-menu {
margin-top: -5px;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1b8eed59a56..261642f4174 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -125,3 +125,29 @@
overflow: hidden;
text-overflow: ellipsis;
}
+
+/*
+ * Mixin for status badges, as used for pipelines and commit signatures
+ */
+@mixin status-color($color-light, $color-main, $color-dark) {
+ color: $color-main;
+ border-color: $color-main;
+
+ &:not(span):hover {
+ background-color: $color-light;
+ color: $color-dark;
+ border-color: $color-dark;
+
+ svg {
+ fill: $color-dark;
+ }
+ }
+
+ svg {
+ fill: $color-main;
+ }
+}
+
+@mixin green-status-color {
+ @include status-color($green-50, $green-500, $green-700);
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e71bf04aec7..35b4d77a5ab 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -182,6 +182,12 @@
}
}
+ &.nav-controls-new-nav {
+ > .dropdown {
+ margin-right: 0;
+ }
+ }
+
> .btn-grouped {
float: none;
}
@@ -190,14 +196,6 @@
display: none;
}
- .btn,
- .dropdown,
- .dropdown-toggle,
- input,
- form {
- height: 35px;
- }
-
input {
display: inline-block;
position: relative;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 542b641e3dd..49b2f0e43a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -92,7 +92,6 @@
@mixin maintain-sidebar-dimensions {
display: block;
width: $gutter-width;
- padding: 10px 0;
}
.issues-bulk-update.right-sidebar {
@@ -104,6 +103,15 @@
&.right-sidebar-expanded {
@include maintain-sidebar-dimensions;
width: $gutter-width;
+
+ .issuable-sidebar-header {
+ // matches `.top-area .nav-controls` for issuable index pages
+ padding: 11px 0;
+ }
+
+ .block:last-of-type {
+ border: none;
+ }
}
&.right-sidebar-collapsed {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8a58c1ed567..bf5f124d142 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -11,8 +11,17 @@
}
img {
- max-width: 100%;
+ /*max-width: 100%;*/
margin: 0 0 8px;
+ min-width: 200px;
+ min-height: 100px;
+ background-color: $gray-lightest;
+ }
+
+ img.js-lazy-loaded {
+ min-width: inherit;
+ min-height: inherit;
+ background-color: inherit;
}
p a:not(.no-attachment-icon) img {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 6b96a88e7ac..80d634487ff 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -380,7 +380,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar
*/
$avatar_radius: 50%;
-$avatar-border: $border-color;
+$avatar-border: $gray-normal;
+$avatar-border-hover: $gray-darker;
+$avatar-background: $gray-lightest;
$gl-avatar-size: 40px;
/*
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index e1873506bec..360ffda8d71 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -21,6 +21,11 @@ header.navbar-gitlab-new {
padding-right: 0;
color: currentColor;
+ img {
+ height: 28px;
+ margin-right: 10px;
+ }
+
> a {
display: flex;
align-items: center;
@@ -41,10 +46,22 @@ header.navbar-gitlab-new {
}
}
+ .logo-text {
+ line-height: initial;
+
+ svg {
+ width: 55px;
+ height: 15px;
+ margin: 0;
+ fill: $white-light;
+ }
+ }
+
&:hover,
&:focus {
- color: $tanuki-yellow;
- text-decoration: none;
+ .logo-text svg {
+ fill: $tanuki-yellow;
+ }
}
}
}
@@ -274,7 +291,7 @@ header.navbar-gitlab-new {
.breadcrumbs {
display: flex;
- min-height: 60px;
+ min-height: 61px;
color: $gl-text-color;
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index ce8f4c41cb5..ae43197a1a6 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -23,6 +23,10 @@ $new-sidebar-width: 220px;
position: fixed;
height: 100%;
}
+
+ .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
+ padding: 10px 0 15px;
+ }
}
.context-header {
@@ -165,7 +169,6 @@ $new-sidebar-width: 220px;
> li {
a {
- font-size: 12px;
padding: 8px 16px 8px 24px;
&:hover,
@@ -262,7 +265,7 @@ $new-sidebar-width: 220px;
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
- height: calc(100vh - 120px);
+ height: calc(100vh - 180px);
// scss-lint:enable DuplicateProperty
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index df858cffe09..6039cda96d8 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -431,7 +431,10 @@
margin: 5px;
}
-.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
+.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
+.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
+ position: absolute;
+
&.right-sidebar {
top: 0;
bottom: 0;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index a5e4c3311f8..cd9f2d787c5 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -54,7 +54,11 @@
.mr-widget-pipeline-graph {
display: inline-block;
vertical-align: middle;
- margin: 0 -6px 0 0;
+ margin-right: 4px;
+
+ .stage-cell .stage-container {
+ margin: 3px 3px 3px 0;
+ }
.dropdown-menu {
margin-top: 11px;
@@ -279,3 +283,63 @@
color: $gl-text-color;
}
}
+
+
+.gpg-status-box {
+ &.valid {
+ @include green-status-color;
+ }
+
+ &.invalid {
+ @include status-color($gray-dark, $gray, $common-gray-dark);
+ border-color: $common-gray-light;
+ }
+}
+
+.gpg-popover-status {
+ display: flex;
+ align-items: center;
+ font-weight: normal;
+ line-height: 1.5;
+}
+
+.gpg-popover-icon {
+ // same margin as .s32.avatar
+ margin-right: $btn-side-margin;
+
+ &.valid {
+ svg {
+ border: 1px solid $brand-success;
+
+ fill: $brand-success;
+ }
+ }
+
+ &.invalid {
+ svg {
+ border: 1px solid $common-gray-light;
+
+ fill: $common-gray-light;
+ }
+ }
+
+ svg {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ vertical-align: middle;
+ }
+}
+
+.gpg-popover-user-link {
+ display: flex;
+ align-items: center;
+ margin-bottom: $gl-padding / 2;
+ text-decoration: none;
+ color: $gl-text-color;
+}
+
+.commit .gpg-popover-help-link {
+ display: block;
+ color: $link-color;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index aa04e490649..eb269df46fe 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -211,6 +211,10 @@
-webkit-overflow-scrolling: touch;
}
+ &.affix-top .issuable-sidebar {
+ height: 100%;
+ }
+
&.right-sidebar-expanded {
width: $gutter_width;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 9637d26e56d..d3862df20d3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -597,7 +597,7 @@
}
// Dropdown button in mini pipeline graph
-.mini-pipeline-graph-dropdown-toggle {
+button.mini-pipeline-graph-dropdown-toggle {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
@@ -608,6 +608,7 @@
padding: 0;
transition: all 0.2s linear;
position: relative;
+ vertical-align: middle;
> .fa.fa-caret-down {
position: absolute;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 22672614e0d..14ad06b0ac2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -391,3 +391,26 @@ table.u2f-registrations {
margin-bottom: 0;
}
}
+
+.gpg-email-badge {
+ display: inline;
+ margin-right: $gl-padding / 2;
+
+ .gpg-email-badge-email {
+ display: inline;
+ margin-right: $gl-padding / 4;
+ }
+
+ .label-verification-status {
+ border-width: 1px;
+ border-style: solid;
+
+ &.verified {
+ @include green-status-color;
+ }
+
+ &.unverified {
+ @include status-color($gray-dark, $gray, $common-gray-dark);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c1423965d0a..b3a90dff89a 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -707,6 +707,7 @@ pre.light-well {
background-color: transparent;
border: 0;
text-align: left;
+ text-overflow: ellipsis;
}
.protected-branches-list,
@@ -742,7 +743,8 @@ pre.light-well {
}
}
-.protected-tags-list {
+.protected-tags-list,
+.protected-branches-list {
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 67ad1ae60af..36f622db136 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,22 +1,3 @@
-@mixin status-color($color-light, $color-main, $color-dark) {
- color: $color-main;
- border-color: $color-main;
-
- &:not(span):hover {
- background-color: $color-light;
- color: $color-dark;
- border-color: $color-dark;
-
- svg {
- fill: $color-dark;
- }
- }
-
- svg {
- fill: $color-main;
- }
-}
-
.ci-status {
padding: 2px 7px 4px;
border: 1px solid $gray-darker;
@@ -41,7 +22,7 @@
}
&.ci-success {
- @include status-color($green-50, $green-500, $green-700);
+ @include green-status-color;
}
&.ci-canceled,
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index c1bc4c0d675..8367c22d1ca 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -76,88 +76,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- application_setting_params_ce
+ visible_application_setting_attributes
)
end
- def application_setting_params_ce
- [
- :admin_notification_email,
- :after_sign_out_path,
- :after_sign_up_text,
- :akismet_api_key,
- :akismet_enabled,
- :container_registry_token_expire_delay,
- :default_artifacts_expire_in,
- :default_branch_protection,
- :default_group_visibility,
- :default_project_visibility,
- :default_projects_limit,
- :default_snippet_visibility,
- :domain_blacklist_enabled,
+ def visible_application_setting_attributes
+ ApplicationSettingsHelper.visible_attributes + [
:domain_blacklist_file,
- :domain_blacklist_raw,
- :domain_whitelist_raw,
- :email_author_in_body,
- :enabled_git_access_protocol,
- :gravatar_enabled,
- :help_page_text,
- :help_page_hide_commercial_content,
- :help_page_support_url,
- :home_page_url,
- :housekeeping_bitmaps_enabled,
- :housekeeping_enabled,
- :housekeeping_full_repack_period,
- :housekeeping_gc_period,
- :housekeeping_incremental_repack_period,
- :html_emails_enabled,
- :koding_enabled,
- :koding_url,
- :password_authentication_enabled,
- :plantuml_enabled,
- :plantuml_url,
- :max_artifacts_size,
- :max_attachment_size,
- :max_pages_size,
- :metrics_enabled,
- :metrics_host,
- :metrics_method_call_threshold,
- :metrics_packet_size,
- :metrics_pool_size,
- :metrics_port,
- :metrics_sample_interval,
- :metrics_timeout,
- :performance_bar_allowed_group_id,
- :performance_bar_enabled,
- :recaptcha_enabled,
- :recaptcha_private_key,
- :recaptcha_site_key,
- :repository_checks_enabled,
- :require_two_factor_authentication,
- :session_expire_delay,
- :sign_in_text,
- :signup_enabled,
- :sentry_dsn,
- :sentry_enabled,
- :clientside_sentry_dsn,
- :clientside_sentry_enabled,
- :send_user_confirmation_email,
- :shared_runners_enabled,
- :shared_runners_text,
- :sidekiq_throttling_enabled,
- :sidekiq_throttling_factor,
- :two_factor_grace_period,
- :user_default_external,
- :user_oauth_applications,
- :unique_ips_limit_per_user,
- :unique_ips_limit_time_window,
- :unique_ips_limit_enabled,
- :version_check_enabled,
- :terminal_max_session_time,
- :polling_interval_multiplier,
- :prometheus_metrics_enabled,
- :usage_ping_enabled,
-
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 434ff6b2a62..16590e66d61 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -50,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def application_params
- params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
+ params.require(:doorkeeper_application).permit(:name, :redirect_uri, :trusted, :scopes)
end
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 8360ce08bdc..05e749c00c0 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,6 +1,6 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.with_route.limit(10)
+ @projects = Project.without_deleted.with_route.limit(10)
@users = User.limit(10)
@groups = Group.with_route.limit(10)
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 984d5398708..0b6cd71e651 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,18 +3,9 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
- params[:sort] ||= 'latest_activity_desc'
- @projects = Project.with_statistics
- @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
- @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
- @projects = @projects.with_push if params[:with_push].present?
- @projects = @projects.abandoned if params[:abandoned].present?
- @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
- @projects = @projects.non_archived unless params[:archived].present?
- @projects = @projects.personal(current_user) if params[:personal].present?
- @projects = @projects.search(params[:name]) if params[:name].present?
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+ finder = Admin::ProjectsFinder.new(params: params, current_user: current_user)
+ @projects = finder.execute
+ @sort = finder.sort
respond_to do |format|
format.html
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 43462b13903..d14b1dbecf6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -70,6 +70,16 @@ class ApplicationController < ActionController::Base
protected
+ def append_info_to_payload(payload)
+ super
+ payload[:remote_ip] = request.remote_ip
+
+ if current_user.present?
+ payload[:user_id] = current_user.id
+ payload[:username] = current_user.username
+ end
+ end
+
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
new file mode 100644
index 00000000000..6779cc6ddac
--- /dev/null
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -0,0 +1,47 @@
+class Profiles::GpgKeysController < Profiles::ApplicationController
+ before_action :set_gpg_key, only: [:destroy, :revoke]
+
+ def index
+ @gpg_keys = current_user.gpg_keys
+ @gpg_key = GpgKey.new
+ end
+
+ def create
+ @gpg_key = current_user.gpg_keys.new(gpg_key_params)
+
+ if @gpg_key.save
+ redirect_to profile_gpg_keys_path
+ else
+ @gpg_keys = current_user.gpg_keys.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ @gpg_key.destroy
+
+ respond_to do |format|
+ format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+
+ def revoke
+ @gpg_key.revoke
+
+ respond_to do |format|
+ format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+
+ private
+
+ def gpg_key_params
+ params.require(:gpg_key).permit(:key)
+ end
+
+ def set_gpg_key
+ @gpg_key = current_user.gpg_keys.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 95de3a44641..221e01b415a 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController
def project
return @project if @project
+ return nil unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 6c25cd83a24..06ba73d8e8d 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -3,11 +3,11 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:index]
before_action :no_cache_headers, except: [:index]
- def build
- build_status = Gitlab::Badge::Build::Status
+ def pipeline
+ pipeline_status = Gitlab::Badge::Pipeline::Status
.new(project, params[:ref])
- render_badge build_status
+ render_badge pipeline_status
end
def coverage
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 37b5a6e9d48..2de9900d449 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -6,18 +6,9 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
+ before_action :set_commits
def show
- @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
- search = params[:search]
-
- @commits =
- if search.present?
- @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
- else
- @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
- end
-
@note_counts = project.notes.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@@ -37,4 +28,33 @@ class Projects::CommitsController < Projects::ApplicationController
end
end
end
+
+ def signatures
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
+ end
+ end
+ end
+
+ private
+
+ def set_commits
+ @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
+ search = params[:search]
+
+ @commits =
+ if search.present?
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
+ else
+ @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
+ end
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 0ac9da2ff0f..e2ccabb22db 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -8,7 +8,6 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new]
- before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
@@ -243,19 +242,19 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_update_issue!
- return render_404 unless can?(current_user, :update_issue, @issue)
+ render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
- return render_404 unless can?(current_user, :admin_issue, @project)
+ render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request!
- return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
def check_issues_available!
- return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
+ return render_404 unless @project.feature_available?(:issues, current_user)
end
def redirect_to_external_issue_tracker
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 70c41da4de5..d361e661d0e 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -223,12 +223,18 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_project_environment_deployment_path(environment.project, environment, deployment)
end
+
+ metrics_monitoring_url =
+ if can?(current_user, :read_environment, environment)
+ environment_metrics_path(environment)
+ end
{
id: environment.id,
name: environment.name,
url: project_environment_path(project, environment),
metrics_url: metrics_url,
+ metrics_monitoring_url: metrics_monitoring_url,
stop_url: stop_url,
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index ea7ceb3eaa5..15a2ff56b92 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -35,7 +35,7 @@ module Projects
def define_badges_variables
@ref = params[:ref] || @project.default_branch || 'master'
- @badges = [Gitlab::Badge::Build::Status,
+ @badges = [Gitlab::Badge::Pipeline::Status,
Gitlab::Badge::Coverage::Report]
@badges.map! do |badge|
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index ac98470c2b1..968d880886c 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -55,6 +55,9 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
+ rescue WikiPage::PageChangedError
+ @conflict = true
+ render 'edit'
end
def create
@@ -119,6 +122,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
- params.require(:wiki).permit(:title, :content, :format, :message)
+ params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index c769693255c..2d7cbd4614e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -296,10 +296,10 @@ class ProjectsController < Projects::ApplicationController
def project_params
params.require(:project)
- .permit(project_params_ce)
+ .permit(project_params_attributes)
end
- def project_params_ce
+ def project_params_attributes
[
:avatar,
:build_allow_git_fetch,
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 0e8a57f8e03..9e743685d60 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_two_factor_requirement, only: [:destroy]
+ # Explicitly call protect from forgery before anything else. Otherwise the
+ # CSFR-token might be cleared before authentication is done. This was the case
+ # when LDAP was enabled and the `OmniauthCallbacksController` is loaded
+ #
+ # *Note:* `prepend: true` is the default for rails4, but this will be changed
+ # to `prepend: false` in rails5.
+ protect_from_forgery prepend: true, with: :exception
+
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
@@ -15,12 +23,7 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- @ldap_servers =
- if Gitlab.config.ldap.enabled
- Gitlab::LDAP::Config.servers
- else
- []
- end
+ @ldap_servers = Gitlab::LDAP::Config.available_servers
super
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 8131eba6a2f..4ee855806ab 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -73,10 +73,7 @@ class UsersController < ApplicationController
end
def calendar
- calendar = contributions_calendar
- @activity_dates = calendar.activity_dates
-
- render 'calendar', layout: false
+ render json: contributions_calendar.activity_dates
end
def calendar_activities
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
new file mode 100644
index 00000000000..a5ba791a513
--- /dev/null
+++ b/app/finders/admin/projects_finder.rb
@@ -0,0 +1,33 @@
+class Admin::ProjectsFinder
+ attr_reader :sort, :namespace_id, :visibility_level, :with_push,
+ :abandoned, :last_repository_check_failed, :archived,
+ :personal, :name, :page, :current_user
+
+ def initialize(params:, current_user:)
+ @current_user = current_user
+ @sort = params.fetch(:sort) { 'latest_activity_desc' }
+ @namespace_id = params[:namespace_id]
+ @visibility_level = params[:visibility_level]
+ @with_push = params[:with_push]
+ @abandoned = params[:abandoned]
+ @last_repository_check_failed = params[:last_repository_check_failed]
+ @archived = params[:archived]
+ @personal = params[:personal]
+ @name = params[:name]
+ @page = params[:page]
+ end
+
+ def execute
+ items = Project.with_statistics
+ items = items.in_namespace(namespace_id) if namespace_id.present?
+ items = items.where(visibility_level: visibility_level) if visibility_level.present?
+ items = items.with_push if with_push.present?
+ items = items.abandoned if abandoned.present?
+ items = items.where(last_repository_check_failed: true) if last_repository_check_failed.present?
+ items = items.non_archived unless archived.present?
+ items = items.personal(current_user) if personal.present?
+ items = items.search(name) if name.present?
+ items = items.sort(sort)
+ items.includes(:namespace).order("namespaces.path, projects.name ASC").page(page)
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index fc63e30c8fb..6fe17a2b99d 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -11,6 +11,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
+# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 2fc34f186ad..771da3d441d 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -10,6 +10,7 @@
# group_id: integer
# project_id: integer
# milestone_title: string
+# author_id: integer
# assignee_id: integer
# search: string
# label_name: string
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 29b88c60dab..6825adcb39f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,4 +1,5 @@
module ApplicationSettingsHelper
+ extend self
delegate :gravatar_enabled?,
:signup_enabled?,
:password_authentication_enabled?,
@@ -91,4 +92,88 @@ module ApplicationSettingsHelper
def sidekiq_queue_options_for_select
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
end
+
+ def visible_attributes
+ [
+ :admin_notification_email,
+ :after_sign_out_path,
+ :after_sign_up_text,
+ :akismet_api_key,
+ :akismet_enabled,
+ :clientside_sentry_dsn,
+ :clientside_sentry_enabled,
+ :container_registry_token_expire_delay,
+ :default_artifacts_expire_in,
+ :default_branch_protection,
+ :default_group_visibility,
+ :default_project_visibility,
+ :default_projects_limit,
+ :default_snippet_visibility,
+ :disabled_oauth_sign_in_sources,
+ :domain_blacklist_enabled,
+ :domain_blacklist_raw,
+ :domain_whitelist_raw,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_hide_commercial_content,
+ :help_page_support_url,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_bitmaps_enabled,
+ :housekeeping_enabled,
+ :housekeeping_full_repack_period,
+ :housekeeping_gc_period,
+ :housekeeping_incremental_repack_period,
+ :html_emails_enabled,
+ :import_sources,
+ :koding_enabled,
+ :koding_url,
+ :max_artifacts_size,
+ :max_attachment_size,
+ :max_pages_size,
+ :metrics_enabled,
+ :metrics_host,
+ :metrics_method_call_threshold,
+ :metrics_packet_size,
+ :metrics_pool_size,
+ :metrics_port,
+ :metrics_sample_interval,
+ :metrics_timeout,
+ :password_authentication_enabled,
+ :performance_bar_allowed_group_id,
+ :performance_bar_enabled,
+ :plantuml_enabled,
+ :plantuml_url,
+ :polling_interval_multiplier,
+ :prometheus_metrics_enabled,
+ :recaptcha_enabled,
+ :recaptcha_private_key,
+ :recaptcha_site_key,
+ :repository_checks_enabled,
+ :repository_storages,
+ :require_two_factor_authentication,
+ :restricted_visibility_levels,
+ :send_user_confirmation_email,
+ :sentry_dsn,
+ :sentry_enabled,
+ :session_expire_delay,
+ :shared_runners_enabled,
+ :shared_runners_text,
+ :sidekiq_throttling_enabled,
+ :sidekiq_throttling_factor,
+ :sidekiq_throttling_queues,
+ :sign_in_text,
+ :signup_enabled,
+ :terminal_max_session_time,
+ :two_factor_grace_period,
+ :unique_ips_limit_enabled,
+ :unique_ips_limit_per_user,
+ :unique_ips_limit_time_window,
+ :usage_ping_enabled,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled
+ ]
+ end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index bbe7f3c8fb4..0e068d4b51c 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
- css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
data_attributes = { container: 'body' }
- if options[:lazy]
- data_attributes[:src] = avatar_url
- end
-
image_tag(
- options[:lazy] ? '' : avatar_url,
- class: "avatar has-tooltip s#{avatar_size} #{css_class}",
+ avatar_url,
+ class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d08e346d605..69220a1c0f6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -113,6 +113,10 @@ module CommitsHelper
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
end
+ def commit_signature_badge_classes(additional_classes)
+ %w(btn status-box gpg-status-box) + Array(additional_classes)
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index fdbca789d21..5f11fe62030 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -61,8 +61,8 @@ module EmailsHelper
else
image_tag(
image_url('mailers/gitlab_header_logo.gif'),
- size: "55x50",
- alt: "GitLab"
+ size: '55x50',
+ alt: 'GitLab'
)
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 425af547330..f4fad7150e8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -354,4 +354,14 @@ module IssuablesHelper
params[:format] = :json if issuable.is_a?(Issue)
end
end
+
+ def issuable_sidebar_options(issuable, can_edit_issuable)
+ {
+ endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ editable: can_edit_issuable,
+ currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
+ rootPath: root_path,
+ fullPath: @project.full_path
+ }
+ end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 42b6cfdf02f..7e1ccb23e9e 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -17,10 +17,10 @@ module IssuesHelper
return '' if project.nil?
url =
- if options[:only_path]
- project.issues_tracker.issue_path(issue_iid)
+ if options[:internal]
+ url_for_internal_issue(issue_iid, project, options)
else
- project.issues_tracker.issue_url(issue_iid)
+ url_for_tracker_issue(issue_iid, project, options)
end
# Ensure we return a valid URL to prevent possible XSS.
@@ -29,6 +29,24 @@ module IssuesHelper
''
end
+ def url_for_tracker_issue(issue_iid, project, options)
+ if options[:only_path]
+ project.issues_tracker.issue_path(issue_iid)
+ else
+ project.issues_tracker.issue_url(issue_iid)
+ end
+ end
+
+ def url_for_internal_issue(issue_iid, project = @project, options = {})
+ helpers = Gitlab::Routing.url_helpers
+
+ if options[:only_path]
+ helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid)
+ else
+ helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid)
+ end
+ end
+
def bulk_update_milestone_options
milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
@@ -158,4 +176,6 @@ module IssuesHelper
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
+ module_function :url_for_internal_issue
+ module_function :url_for_tracker_issue
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
new file mode 100644
index 00000000000..2c5619ac41b
--- /dev/null
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -0,0 +1,24 @@
+module LazyImageTagHelper
+ def placeholder_image
+ "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+ end
+
+ # Override the default ActionView `image_tag` helper to support lazy-loading
+ def image_tag(source, options = {})
+ options = options.symbolize_keys
+
+ unless options.delete(:lazy) == false
+ options[:data] ||= {}
+ options[:data][:src] = path_to_image(source)
+ options[:class] ||= ""
+ options[:class] << " lazy"
+
+ source = placeholder_image
+ end
+
+ super(source, options)
+ end
+
+ # Required for Banzai::Filter::ImageLazyLoadFilter
+ module_function :placeholder_image
+end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 0a0881d95cf..505579674c9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -130,4 +130,14 @@ module NotesHelper
can?(current_user, :create_note, @project)
end
end
+
+ def initial_notes_data(autocomplete)
+ {
+ notesUrl: notes_url,
+ notesIds: @notes.map(&:id),
+ now: Time.now.to_i,
+ diffView: diff_view,
+ autocomplete: autocomplete
+ }
+ end
end
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 209bd56b78a..08fd97cd048 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -18,7 +18,8 @@ module SystemNoteHelper
'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right',
- 'outdated' => 'icon_edit'
+ 'outdated' => 'icon_edit',
+ 'duplicate' => 'icon_clone'
}.freeze
def icon_for_system_note(note)
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index a48d4475e97..ce435ca2241 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -8,6 +8,6 @@ module TriggersHelper
end
def service_trigger_url(service)
- "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger"
+ "#{Settings.gitlab.url}/api/v4/projects/#{service.project_id}/services/#{service.to_param}/trigger"
end
end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index 456598b4c28..3b175251446 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url
- image_tag image_url, class: 'js-version-status-badge'
+ image_tag image_url, class: 'js-version-status-badge', lazy: false
end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 256cbcd73a1..c401030e34a 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -14,7 +14,7 @@ module Emails
end
def new_ssh_key_email(key_id)
- @key = Key.find_by_id(key_id)
+ @key = Key.find_by(id: key_id)
return unless @key
@@ -22,5 +22,15 @@ module Emails
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end
+
+ def new_gpg_key_email(gpg_key_id)
+ @gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return unless @gpg_key
+
+ @current_user = @user = @gpg_key.user
+ @target_url = user_url(@user)
+ mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
+ end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 898ce45f60e..bd7c4cd45ea 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -315,7 +315,9 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
+ # DEPRECATED
# repository_storage is still required in the API. Remove in 9.0
+ # Still used in API v3
def repository_storage
repository_storages.first
end
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
index c52b6f15913..25ecf2d5937 100644
--- a/app/models/chat_team.rb
+++ b/app/models/chat_team.rb
@@ -3,4 +3,13 @@ class ChatTeam < ActiveRecord::Base
validates :namespace, uniqueness: true
belongs_to :namespace
+
+ def remove_mattermost_team(current_user)
+ Mattermost::Team.new(current_user).destroy(team_id: team_id)
+ rescue Mattermost::ClientError => e
+ # Either the group is not found, or the user doesn't have the proper
+ # access on the mattermost instance. In the first case, we're done either way
+ # in the latter case, we can't recover by retrying, so we just log what happened
+ Rails.logger.error("Mattermost team deletion failed: #{e}")
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 416a2a33378..8be2dee6479 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -219,6 +219,7 @@ module Ci
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
variables += secret_variables(environment: environment)
variables += trigger_request.user_variables if trigger_request
+ variables += pipeline.variables.map(&:to_runner_variable)
variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b646b32fc64..d2abcf30034 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -15,13 +15,14 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :variables, class_name: 'Ci::PipelineVariable'
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
has_many :merge_requests, foreign_key: "head_pipeline_id"
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
- has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index e4ae1b35f66..085eeeae157 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -40,10 +40,6 @@ module Ci
update_attribute(:active, false)
end
- def runnable_by_owner?
- Ability.allowed?(owner, :create_pipeline, project)
- end
-
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
new file mode 100644
index 00000000000..00b419c3efa
--- /dev/null
+++ b/app/models/ci/pipeline_variable.rb
@@ -0,0 +1,10 @@
+module Ci
+ class PipelineVariable < ActiveRecord::Base
+ extend Ci::Model
+ include HasVariable
+
+ belongs_to :pipeline
+
+ validates :key, uniqueness: { scope: :pipeline_id }
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1e19f00106a..7940733f557 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -234,6 +234,14 @@ class Commit
@statuses[ref] = pipelines.latest_status(ref)
end
+ def signature
+ return @signature if defined?(@signature)
+
+ @signature = gpg_commit.signature
+ end
+
+ delegate :has_signature?, to: :gpg_commit
+
def revert_branch_name
"revert-#{short_id}"
end
@@ -382,4 +390,8 @@ class Commit
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
+
+ def gpg_commit
+ @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 95152dcd68c..48547a938fc 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 1
+ CACHE_VERSION = 2
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index fc6b840f7a8..ef95d6b0f98 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -17,7 +17,13 @@ module ProtectedRef
class_methods do
def protected_ref_access_levels(*types)
types.each do |type|
- has_many :"#{type}_access_levels", dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # We need to set `inverse_of` to make sure the `belongs_to`-object is set
+ # when creating children using `accepts_nested_attributes_for`.
+ #
+ # If we don't `protected_branch` or `protected_tag` would be empty and
+ # `project` cannot be delegated to it, which in turn would cause validations
+ # to fail.
+ has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
@@ -25,8 +31,8 @@ module ProtectedRef
end
end
- def protected_ref_accessible_to?(ref, user, action:)
- access_levels_for_ref(ref, action: action).any? do |access_level|
+ def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
+ access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user)
end
end
@@ -37,8 +43,9 @@ module ProtectedRef
end
end
- def access_levels_for_ref(ref, action:)
- self.matching(ref).map(&:"#{action}_access_levels").flatten
+ def access_levels_for_ref(ref, action:, protected_refs: nil)
+ self.matching(ref, protected_refs: protected_refs)
+ .map(&:"#{action}_access_levels").flatten
end
def matching(ref_name, protected_refs: nil)
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
new file mode 100644
index 00000000000..3df60ddc950
--- /dev/null
+++ b/app/models/gpg_key.rb
@@ -0,0 +1,107 @@
+class GpgKey < ActiveRecord::Base
+ KEY_PREFIX = '-----BEGIN PGP PUBLIC KEY BLOCK-----'.freeze
+ KEY_SUFFIX = '-----END PGP PUBLIC KEY BLOCK-----'.freeze
+
+ include ShaAttribute
+
+ sha_attribute :primary_keyid
+ sha_attribute :fingerprint
+
+ belongs_to :user
+ has_many :gpg_signatures
+
+ validates :user, presence: true
+
+ validates :key,
+ presence: true,
+ uniqueness: true,
+ format: {
+ with: /\A#{KEY_PREFIX}((?!#{KEY_PREFIX})(?!#{KEY_SUFFIX}).)+#{KEY_SUFFIX}\Z/m,
+ message: "is invalid. A valid public GPG key begins with '#{KEY_PREFIX}' and ends with '#{KEY_SUFFIX}'"
+ }
+
+ validates :fingerprint,
+ presence: true,
+ uniqueness: true,
+ # only validate when the `key` is valid, as we don't want the user to show
+ # the error about the fingerprint
+ unless: -> { errors.has_key?(:key) }
+
+ validates :primary_keyid,
+ presence: true,
+ uniqueness: true,
+ # only validate when the `key` is valid, as we don't want the user to show
+ # the error about the fingerprint
+ unless: -> { errors.has_key?(:key) }
+
+ before_validation :extract_fingerprint, :extract_primary_keyid
+ after_commit :update_invalid_gpg_signatures, on: :create
+ after_commit :notify_user, on: :create
+
+ def primary_keyid
+ super&.upcase
+ end
+
+ def fingerprint
+ super&.upcase
+ end
+
+ def key=(value)
+ super(value&.strip)
+ end
+
+ def user_infos
+ @user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
+ end
+
+ def verified_user_infos
+ user_infos.select do |user_info|
+ user_info[:email] == user.email
+ end
+ end
+
+ def emails_with_verified_status
+ user_infos.map do |user_info|
+ [
+ user_info[:email],
+ user_info[:email] == user.email
+ ]
+ end.to_h
+ end
+
+ def verified?
+ emails_with_verified_status.any? { |_email, verified| verified }
+ end
+
+ def update_invalid_gpg_signatures
+ InvalidGpgSignatureUpdateWorker.perform_async(self.id)
+ end
+
+ def revoke
+ GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
+ gpg_key_id: nil,
+ valid_signature: false,
+ updated_at: Time.zone.now
+ )
+
+ destroy
+ end
+
+ private
+
+ def extract_fingerprint
+ # we can assume that the result only contains one item as the validation
+ # only allows one key
+ self.fingerprint = Gitlab::Gpg.fingerprints_from_key(key).first
+ end
+
+ def extract_primary_keyid
+ # we can assume that the result only contains one item as the validation
+ # only allows one key
+ self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
+ end
+
+ def notify_user
+ NotificationService.new.new_gpg_key(self)
+ end
+end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
new file mode 100644
index 00000000000..1ac0e123ff1
--- /dev/null
+++ b/app/models/gpg_signature.rb
@@ -0,0 +1,21 @@
+class GpgSignature < ActiveRecord::Base
+ include ShaAttribute
+
+ sha_attribute :commit_sha
+ sha_attribute :gpg_key_primary_keyid
+
+ belongs_to :project
+ belongs_to :gpg_key
+
+ validates :commit_sha, presence: true
+ validates :project_id, presence: true
+ validates :gpg_key_primary_keyid, presence: true
+
+ def gpg_key_primary_keyid
+ super&.upcase
+ end
+
+ def commit
+ project.commit(commit_sha)
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index dfa4e8adedd..bd5735ed82e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -167,10 +167,14 @@ class Group < Namespace
end
def has_owner?(user)
+ return false unless user
+
members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
+ return false unless user
+
members_with_parents.masters.where(user_id: user).any?
end
@@ -212,7 +216,7 @@ class Group < Namespace
end
def members_with_parents
- GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id))
+ GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
end
def users_with_parents
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e4e7999d0f2..a910099b4c1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -596,7 +596,7 @@ class MergeRequest < ActiveRecord::Base
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user)
- return if project.has_external_issue_tracker?
+ return unless project.issues_enabled?
transaction do
self.merge_requests_closing_issues.delete_all
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 4b141945ab4..ec87aee9310 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -236,10 +236,21 @@ class MergeRequestDiff < ActiveRecord::Base
def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index|
- diff.to_hash.merge(
+ diff_hash = diff.to_hash.merge(
+ binary: false,
merge_request_diff_id: self.id,
relative_order: index
)
+
+ # Compatibility with old diffs created with Psych.
+ diff_hash.tap do |hash|
+ diff_text = hash[:diff]
+
+ if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?
+ hash[:binary] = true
+ hash[:diff] = [diff_text].pack('m0')
+ end
+ end
end
Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
@@ -268,9 +279,7 @@ class MergeRequestDiff < ActiveRecord::Base
st_diffs
end
elsif merge_request_diff_files.present?
- merge_request_diff_files
- .as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS)
- .map(&:with_indifferent_access)
+ merge_request_diff_files.map(&:to_hash)
end
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 598ebd4d829..1199ff5af22 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -8,4 +8,14 @@ class MergeRequestDiffFile < ActiveRecord::Base
encode_utf8(diff) if diff.respond_to?(:encoding)
end
+
+ def diff
+ binary? ? super.unpack('m0').first : super
+ end
+
+ def to_hash
+ keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
+
+ as_json(only: keys).merge(diff: diff).with_indifferent_access
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0b357d5d003..d827bfaa806 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -734,9 +734,11 @@ class Project < ActiveRecord::Base
end
def get_issue(issue_id, current_user)
- if default_issues_tracker?
- IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
- else
+ issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?
+
+ if issue
+ issue
+ elsif external_issue_tracker
ExternalIssue.new(issue_id, self)
end
end
@@ -758,7 +760,7 @@ class Project < ActiveRecord::Base
end
def external_issue_reference_pattern
- external_issue_tracker.class.reference_pattern
+ external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
end
def default_issues_tracker?
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 6d6a3ae3647..31984c5d7ed 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -8,8 +8,12 @@ class IssueTrackerService < Service
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
- def self.reference_pattern
- @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ def self.reference_pattern(only_long: false)
+ if only_long
+ %r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)}
+ else
+ %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ end
end
def default?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5498a2e17b2..2aa19443198 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -3,10 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
- validates :project_key, presence: true, if: :activated?
- prop_accessor :username, :password, :url, :api_url, :project_key,
- :jira_issue_transition_id, :title, :description
+ prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
before_update :reset_password
@@ -18,7 +16,7 @@ class JiraService < IssueTrackerService
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def self.reference_pattern
+ def self.reference_pattern(only_long: true)
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
@@ -54,10 +52,6 @@ class JiraService < IssueTrackerService
@client ||= JIRA::Client.new(options)
end
- def jira_project
- @jira_project ||= jira_request { client.Project.find(project_key) }
- end
-
def help
"You need to configure JIRA before enabling this service. For more details
read the
@@ -88,18 +82,12 @@ class JiraService < IssueTrackerService
[
{ type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
- { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
{ type: 'text', name: 'username', placeholder: '', required: true },
{ type: 'password', name: 'password', placeholder: '', required: true },
- { type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
+ { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID', placeholder: '' }
]
end
- # URLs to redirect from Gitlab issues pages to jira issue tracker
- def project_url
- "#{url}/issues/?jql=project=#{project_key}"
- end
-
def issues_url
"#{url}/browse/:id"
end
@@ -172,7 +160,10 @@ class JiraService < IssueTrackerService
def test(_)
result = test_settings
- { success: result.present?, result: result }
+ success = result.present?
+ result = @error if @error && !success
+
+ { success: success, result: result }
end
# JIRA does not need test data.
@@ -184,7 +175,7 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
# Test settings by getting the project
- jira_request { jira_project.present? }
+ jira_request { client.ServerInfo.all.attrs }
end
private
@@ -300,7 +291,8 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
+ @error = e.message
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
nil
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 8663cf5e602..50b7a477904 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -457,10 +457,6 @@ class Repository
nil
end
- def blob_by_oid(oid)
- Gitlab::Git::Blob.raw(self, oid)
- end
-
def root_ref
if raw_repository
raw_repository.root_ref
@@ -471,8 +467,17 @@ class Repository
end
cache_method :root_ref
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314
def exists?
- refs_directory_exists?
+ return false unless path_with_namespace
+
+ Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ if enabled
+ raw_repository.exists?
+ else
+ refs_directory_exists?
+ end
+ end
end
cache_method :exists?
@@ -1095,8 +1100,6 @@ class Repository
end
def refs_directory_exists?
- return false unless path_with_namespace
-
File.exist?(File.join(path_to_repo, 'refs'))
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 414c95f7705..0b33e45473b 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,7 +1,8 @@
class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
- title time_tracking branch milestone discussion task moved opened closed merged
+ title time_tracking branch milestone discussion task moved
+ opened closed merged duplicate
outdated
].freeze
diff --git a/app/models/user.rb b/app/models/user.rb
index c26be6d05a2..6e66c587a1f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -76,6 +76,7 @@ class User < ActiveRecord::Base
where(type.not_eq('DeployKey').or(type.eq(nil)))
end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -157,6 +158,7 @@ class User < ActiveRecord::Base
before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
after_save :ensure_namespace_correct
+ after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
after_initialize :set_projects_limit
after_destroy :post_destroy_hook
@@ -512,6 +514,10 @@ class User < ActiveRecord::Base
end
end
+ def update_invalid_gpg_signatures
+ gpg_keys.each(&:update_invalid_gpg_signatures)
+ end
+
# Returns the groups a user has access to
def authorized_groups
union = Gitlab::SQL::Union
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 224eb3cd4d0..148998bc9be 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,4 +1,6 @@
class WikiPage
+ PageChangedError = Class.new(StandardError)
+
include ActiveModel::Validations
include ActiveModel::Conversion
include StaticModel
@@ -136,6 +138,10 @@ class WikiPage
versions.first
end
+ def last_commit_sha
+ commit&.sha
+ end
+
# Returns the Date that this latest version was
# created on.
def created_at
@@ -182,17 +188,22 @@ class WikiPage
# Updates an existing Wiki Page, creating a new version.
#
- # new_content - The raw markup content to replace the existing.
- # format - Optional symbol representing the content format.
- # See ProjectWiki::MARKUPS Hash for available formats.
- # message - Optional commit message to set on the new version.
+ # new_content - The raw markup content to replace the existing.
+ # format - Optional symbol representing the content format.
+ # See ProjectWiki::MARKUPS Hash for available formats.
+ # message - Optional commit message to set on the new version.
+ # last_commit_sha - Optional last commit sha to validate the page unchanged.
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
- def update(new_content = "", format = :markdown, message = nil)
+ def update(new_content, format: :markdown, message: nil, last_commit_sha: nil)
@attributes[:content] = new_content
@attributes[:format] = format
+ if last_commit_sha && last_commit_sha != self.last_commit_sha
+ raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
+ end
+
save :update_page, @page, content, format, message
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 386822d3ff6..984e5482288 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,17 +1,15 @@
module Ci
class BuildPolicy < CommitStatusPolicy
- condition(:protected_action) do
- next false unless @subject.action?
-
+ condition(:protected_ref) do
access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
if @subject.tag?
!access.can_create_tag?(@subject.ref)
else
- !access.can_merge_to_branch?(@subject.ref)
+ !access.can_update_branch?(@subject.ref)
end
end
- rule { protected_action }.prevent :update_build
+ rule { protected_ref }.prevent :update_build
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index a2dde95dbc8..4e689a9efd5 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -1,5 +1,17 @@
module Ci
class PipelinePolicy < BasePolicy
delegate { @subject.project }
+
+ condition(:protected_ref) do
+ access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
+
+ if @subject.tag?
+ !access.can_create_tag?(@subject.ref)
+ else
+ !access.can_update_branch?(@subject.ref)
+ end
+ end
+
+ rule { protected_ref }.prevent :update_pipeline
end
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 55eefa76d3f..1c91425f589 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -44,7 +44,7 @@ class GlobalPolicy < BasePolicy
prevent :log_in
end
- rule { ~restricted_public_level }.policy do
+ rule { admin | ~restricted_public_level }.policy do
enable :read_users_list
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 323131c0f7e..0133091db57 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -10,7 +10,8 @@ class ProjectPolicy < BasePolicy
desc "User is a project owner"
condition :owner do
- @user && project.owner == @user || (project.group && project.group.has_owner?(@user))
+ (project.owner.present? && project.owner == @user) ||
+ project.group&.has_owner?(@user)
end
desc "Project has public builds enabled"
@@ -287,9 +288,6 @@ class ProjectPolicy < BasePolicy
prevent :create_issue
prevent :update_issue
prevent :admin_issue
- end
-
- rule { issues_disabled & default_issues_tracker }.policy do
prevent :read_issue
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 20f9938f038..743a08acefe 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -16,7 +16,8 @@ class BuildDetailsEntity < JobEntity
end
expose :path do |build|
- project_merge_request_path(project, build.merge_request)
+ project_merge_request_path(build.merge_request.project,
+ build.merge_request)
end
end
diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb
index 068013c8829..c75431a79ae 100644
--- a/app/serializers/deploy_key_entity.rb
+++ b/app/serializers/deploy_key_entity.rb
@@ -9,7 +9,7 @@ class DeployKeyEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :projects, using: ProjectEntity do |deploy_key|
- deploy_key.projects.select { |project| options[:user].can?(:read_project, project) }
+ deploy_key.projects.without_deleted.select { |project| options[:user].can?(:read_project, project) }
end
expose :can_edit
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 273386776fa..884b681ff81 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -15,12 +15,48 @@ module Ci
pipeline_schedule: schedule
)
+ result = validate(current_user || trigger_request.trigger.owner,
+ ignore_skip_ci: ignore_skip_ci,
+ save_on_errors: save_on_errors)
+
+ return result if result
+
+ begin
+ Ci::Pipeline.transaction do
+ pipeline.save!
+
+ yield(pipeline) if block_given?
+
+ Ci::CreatePipelineStagesService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ return error("Failed to persist the pipeline: #{e}")
+ end
+
+ update_merge_requests_head_pipeline
+
+ cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
+
+ pipeline_created_counter.increment(source: source)
+
+ pipeline.tap(&:process!)
+ end
+
+ private
+
+ def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
unless project.builds_enabled?
return error('Pipeline is disabled')
end
- unless trigger_request || can?(current_user, :create_pipeline, project)
- return error('Insufficient permissions to create a new pipeline')
+ unless allowed_to_trigger_pipeline?(triggering_user)
+ if can?(triggering_user, :create_pipeline, project)
+ return error("Insufficient permissions for protected ref '#{ref}'")
+ else
+ return error('Insufficient permissions to create a new pipeline')
+ end
end
unless branch? || tag?
@@ -46,24 +82,29 @@ module Ci
unless pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
+ end
- Ci::Pipeline.transaction do
- update_merge_requests_head_pipeline if pipeline.save
-
- Ci::CreatePipelineStagesService
- .new(project, current_user)
- .execute(pipeline)
+ def allowed_to_trigger_pipeline?(triggering_user)
+ if triggering_user
+ allowed_to_create?(triggering_user)
+ else # legacy triggers don't have a corresponding user
+ !project.protected_for?(ref)
end
+ end
- cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
+ def allowed_to_create?(triggering_user)
+ access = Gitlab::UserAccess.new(triggering_user, project: project)
- pipeline_created_counter.increment(source: source)
-
- pipeline.tap(&:process!)
+ can?(triggering_user, :create_pipeline, project) &&
+ if branch?
+ access.can_update_branch?(ref)
+ elsif tag?
+ access.can_create_tag?(ref)
+ else
+ true # Allow it for now and we'll reject when we check ref existence
+ end
end
- private
-
def update_merge_requests_head_pipeline
return unless pipeline.latest?
@@ -113,15 +154,21 @@ module Ci
end
def branch?
- project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
+ return @is_branch if defined?(@is_branch)
+
+ @is_branch =
+ project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
end
def tag?
- project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+ return @is_tag if defined?(@is_tag)
+
+ @is_tag =
+ project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
end
def ref
- Gitlab::Git.ref_name(origin_ref)
+ @ref ||= Gitlab::Git.ref_name(origin_ref)
end
def valid_sha?
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index cf3d4aee2bc..b2aa457bbd5 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,12 +1,19 @@
+# This class is deprecated because we're closing Ci::TriggerRequest.
+# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb)
+# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest.
+# We remove this class after we removed v1 and v3 API. This class is still being
+# referred by such legacy code.
module Ci
- class CreateTriggerRequestService
- def execute(project, trigger, ref, variables = nil)
+ module CreateTriggerRequestService
+ Result = Struct.new(:trigger_request, :pipeline)
+
+ def self.execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables)
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref)
.execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
- trigger_request if pipeline.persisted?
+ Result.new(trigger_request, pipeline)
end
end
end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
new file mode 100644
index 00000000000..1e5ad28ba57
--- /dev/null
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -0,0 +1,44 @@
+module Ci
+ class PipelineTriggerService < BaseService
+ def execute
+ if trigger_from_token
+ create_pipeline_from_trigger(trigger_from_token)
+ end
+ end
+
+ private
+
+ def create_pipeline_from_trigger(trigger)
+ # this check is to not leak the presence of the project if user cannot read it
+ return unless trigger.project == project
+
+ pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
+ .execute(:trigger, ignore_skip_ci: true) do |pipeline|
+ trigger.trigger_requests.create!(pipeline: pipeline)
+ create_pipeline_variables!(pipeline)
+ end
+
+ if pipeline.persisted?
+ success(pipeline: pipeline)
+ else
+ error(pipeline.errors.messages, 400)
+ end
+ end
+
+ def trigger_from_token
+ return @trigger if defined?(@trigger)
+
+ @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ end
+
+ def create_pipeline_variables!(pipeline)
+ return unless params[:variables]
+
+ variables = params[:variables].map do |key, value|
+ { key: key, value: value }
+ end
+
+ pipeline.variables.create!(variables)
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 20d1fb29289..bb7680c5054 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -56,6 +56,8 @@ class GitPushService < BaseService
perform_housekeeping
update_caches
+
+ update_signatures
end
def update_gitattributes
@@ -80,6 +82,12 @@ class GitPushService < BaseService
ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
end
+ def update_signatures
+ @push_commits.each do |commit|
+ CreateGpgSignatureWorker.perform_async(commit.sha, @project.id)
+ end
+ end
+
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 80c51cb5a72..f565612a89d 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -21,6 +21,8 @@ module Groups
DestroyService.new(group, current_user).execute
end
+ group.chat_team&.remove_mattermost_team(current_user)
+
group.really_destroy!
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 9078b1f0983..ea497729115 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -58,6 +58,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_ids)
params.delete(:assignee_id)
params.delete(:due_date)
+ params.delete(:canonical_issue_id)
end
filter_assignee(issuable)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 34199eb5d13..4c198fc96ea 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -7,6 +7,14 @@ module Issues
issue_data
end
+ def reopen_service
+ Issues::ReopenService
+ end
+
+ def close_service
+ Issues::CloseService
+ end
+
private
def create_assignee_note(issue, old_assignees)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ddef5281498..74459c3342c 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -16,13 +16,13 @@ module Issues
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
- if project.jira_tracker? && project.jira_service.active
+ if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
project.jira_service.close_issue(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
- if project.default_issues_tracker? && issue.close
+ if project.issues_enabled? && issue.close
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb
new file mode 100644
index 00000000000..5c0854e664d
--- /dev/null
+++ b/app/services/issues/duplicate_service.rb
@@ -0,0 +1,24 @@
+module Issues
+ class DuplicateService < Issues::BaseService
+ def execute(duplicate_issue, canonical_issue)
+ return if canonical_issue == duplicate_issue
+ return unless can?(current_user, :update_issue, duplicate_issue)
+ return unless can?(current_user, :create_note, canonical_issue)
+
+ create_issue_duplicate_note(duplicate_issue, canonical_issue)
+ create_issue_canonical_note(canonical_issue, duplicate_issue)
+
+ close_service.new(project, current_user, {}).execute(duplicate_issue)
+ end
+
+ private
+
+ def create_issue_duplicate_note(duplicate_issue, canonical_issue)
+ SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue)
+ end
+
+ def create_issue_canonical_note(canonical_issue, duplicate_issue)
+ SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index cd9f9a4a16e..8d918ccc635 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -5,6 +5,7 @@ module Issues
def execute(issue)
handle_move_between_iids(issue)
filter_spam_check_params
+ change_issue_duplicate(issue)
update(issue)
end
@@ -53,14 +54,6 @@ module Issues
end
end
- def reopen_service
- Issues::ReopenService
- end
-
- def close_service
- Issues::CloseService
- end
-
def handle_move_between_iids(issue)
return unless params[:move_between_iids]
@@ -72,6 +65,15 @@ module Issues
issue.move_between(issue_before, issue_after)
end
+ def change_issue_duplicate(issue)
+ canonical_issue_id = params.delete(:canonical_issue_id)
+ canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
+
+ if canonical_issue
+ Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue)
+ end
+ end
+
private
def get_issue_if_allowed(project, iid)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3a98a5f6b64..b94921d2a08 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -17,6 +17,16 @@ class NotificationService
end
end
+ # Always notify the user about gpg key added
+ #
+ # This is a security email so it will be sent even if the user user disabled
+ # notifications
+ def new_gpg_key(gpg_key)
+ if gpg_key.user
+ mailer.new_gpg_key_email(gpg_key.id).deliver_later
+ end
+ end
+
# Always notify user about email added to profile
def new_email(email)
if email.user
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index e2b2660ea71..f6e8b6655f2 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -15,40 +15,48 @@ module Projects
def execute
return false unless can?(current_user, :remove_project, project)
- repo_path = project.path_with_namespace
- wiki_path = repo_path + '.wiki'
-
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
- flush_caches(project, wiki_path)
+ flush_caches(project)
Projects::UnlinkForkService.new(project, current_user).execute
- Project.transaction do
- project.team.truncate
- project.destroy!
-
- unless remove_legacy_registry_tags
- raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
- end
-
- unless remove_repository(repo_path)
- raise_error('Failed to remove project repository. Please try again or contact administrator.')
- end
+ attempt_destroy_transaction(project)
- unless remove_repository(wiki_path)
- raise_error('Failed to remove wiki repository. Please try again or contact administrator.')
- end
- end
-
- log_info("Project \"#{project.path_with_namespace}\" was removed")
system_hook_service.execute_hooks_for(project, :destroy)
+ log_info("Project \"#{project.full_path}\" was removed")
+
true
+ rescue => error
+ attempt_rollback(project, error.message)
+ false
+ rescue Exception => error # rubocop:disable Lint/RescueException
+ # Project.transaction can raise Exception
+ attempt_rollback(project, error.message)
+ raise
end
private
+ def repo_path
+ project.path_with_namespace
+ end
+
+ def wiki_path
+ repo_path + '.wiki'
+ end
+
+ def trash_repositories!
+ unless remove_repository(repo_path)
+ raise_error('Failed to remove project repository. Please try again or contact administrator.')
+ end
+
+ unless remove_repository(wiki_path)
+ raise_error('Failed to remove wiki repository. Please try again or contact administrator.')
+ end
+ end
+
def remove_repository(path)
# Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true
@@ -70,6 +78,26 @@ module Projects
end
end
+ def attempt_rollback(project, message)
+ return unless project
+
+ project.update_attributes(delete_error: message, pending_delete: false)
+ log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
+ end
+
+ def attempt_destroy_transaction(project)
+ Project.transaction do
+ unless remove_legacy_registry_tags
+ raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.')
+ end
+
+ trash_repositories!
+
+ project.team.truncate
+ project.destroy!
+ end
+ end
+
##
# This method makes sure that we correctly remove registry tags
# for legacy image repository (when repository path equals project path).
@@ -96,7 +124,7 @@ module Projects
"#{path}+#{project.id}#{DELETED_FLAG}"
end
- def flush_caches(project, wiki_path)
+ def flush_caches(project)
project.repository.before_delete
Repository.new(wiki_path, project).before_delete
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index e60b854f916..749a1cc56d8 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -130,7 +130,11 @@ module Projects
end
def max_size
- current_application_settings.max_pages_size.megabytes || MAX_SIZE
+ max_pages_size = current_application_settings.max_pages_size.megabytes
+
+ return MAX_SIZE if max_pages_size.zero?
+
+ [max_pages_size, MAX_SIZE].min
end
def tmp_path
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 30ca95eef7a..d81035e4eba 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -5,7 +5,7 @@ module Projects
return error('New visibility level not allowed!')
end
- if project.has_container_registry_tags?
+ if renaming_project_with_container_registry_tags?
return error('Cannot rename project because it contains container registry tags!')
end
@@ -44,6 +44,13 @@ module Projects
true
end
+ def renaming_project_with_container_registry_tags?
+ new_path = params[:path]
+
+ new_path && new_path != project.path &&
+ project.has_container_registry_tags?
+ end
+
def changing_default_branch?
new_branch = params[:default_branch]
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6f82159e6c7..5dc1b91d2c0 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -471,6 +471,24 @@ module QuickActions
end
end
+ desc 'Mark this issue as a duplicate of another issue'
+ explanation do |duplicate_reference|
+ "Marks this issue as a duplicate of #{duplicate_reference}."
+ end
+ params '#issue'
+ condition do
+ issuable.is_a?(Issue) &&
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :duplicate do |duplicate_param|
+ canonical_issue = extract_references(duplicate_param, :issue).first
+
+ if canonical_issue.present?
+ @updates[:canonical_issue_id] = canonical_issue.id
+ end
+ end
+
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index da0f21d449a..2dbee9c246e 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -552,6 +552,44 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
+ # Called when a Noteable has been marked as a duplicate of another Issue
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # canonical_issue - Issue that this is a duplicate of
+ #
+ # Example Note text:
+ #
+ # "marked this issue as a duplicate of #1234"
+ #
+ # "marked this issue as a duplicate of other_project#5678"
+ #
+ # Returns the created Note object
+ def mark_duplicate_issue(noteable, project, author, canonical_issue)
+ body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ # Called when a Noteable has been marked as the canonical Issue of a duplicate
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # duplicate_issue - Issue that was a duplicate of this
+ #
+ # Example Note text:
+ #
+ # "marked #1234 as a duplicate of this issue"
+ #
+ # "marked other_project#5678 as a duplicate of this issue"
+ #
+ # Returns the created Note object
+ def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
+ body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index 8f6a50da838..c628e6781af 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -1,7 +1,7 @@
module WikiPages
class UpdateService < WikiPages::BaseService
def execute(page)
- if page.update(@params[:content], @params[:format], @params[:message])
+ if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha])
execute_hooks(page, 'update')
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 26f7c1a473a..8bb2a563990 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -315,7 +315,9 @@
%fieldset
%legend Metrics - Prometheus
%p
- Enable a Prometheus metrics endpoint at `#{metrics_path}` to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available
+ Enable a Prometheus metrics endpoint at
+ %code= metrics_path
+ to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available
= link_to 'here', admin_health_check_path
\. This setting requires a
= link_to 'restart', help_page_path('administration/restart_gitlab')
@@ -327,10 +329,13 @@
= f.label :prometheus_metrics_enabled do
= f.check_box :prometheus_metrics_enabled
Enable Prometheus Metrics
- - unless Gitlab::Metrics.metrics_folder_present?
- .help-block
- %strong.cred WARNING:
- Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
+ - unless Gitlab::Metrics.metrics_folder_present?
+ .help-block
+ %strong.cred WARNING:
+ Environment variable
+ %code prometheus_multiproc_dir
+ does not exist or is not pointing to a valid directory.
+ = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
%fieldset
%legend Profiling - Performance Bar
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 061f8991b11..93827d6a1ab 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -6,6 +6,7 @@
.col-sm-10
= f.text_field :name, class: 'form-control'
= doorkeeper_errors_for application, :name
+
= content_tag :div, class: 'form-group' do
= f.label :redirect_uri, class: 'col-sm-2 control-label'
.col-sm-10
@@ -19,6 +20,13 @@
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
+ = content_tag :div, class: 'form-group' do
+ = f.label :trusted, class: 'col-sm-2 control-label'
+ .col-sm-10
+ = f.check_box :trusted
+ %span.help-block
+ Trusted applications are automatically authorized on GitLab OAuth flow.
+
.form-group
= f.label :scopes, class: 'col-sm-2 control-label'
.col-sm-10
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index eb4293c7e37..94d33fa6489 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -11,6 +11,7 @@
%th Name
%th Callback URL
%th Clients
+ %th Trusted
%th
%th
%tbody.oauth-applications
@@ -19,5 +20,6 @@
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
%td= application.access_tokens.map(&:resource_owner_id).uniq.count
+ %td= application.trusted? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= render 'delete_form', application: application
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 14683cc66e9..5125aa21b06 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -23,6 +23,12 @@
%div
%span.monospace= uri
+ %tr
+ %td
+ Trusted
+ %td
+ = @application.trusted? ? 'Y' : 'N'
+
= render "shared/tokens/scopes_list", token: @application
.form-actions
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 128b5dc01ab..8e94e68bc11 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -150,7 +150,7 @@
.well-segment.well-centered
= link_to admin_groups_path do
%h3.text-center
- Groups
+ Groups:
= number_with_delimiter(Group.count)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2da8f615470..126550ee10e 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -2,26 +2,6 @@
= render "admin/dashboard/head"
%div{ class: container_class }
-
- %p.prepend-top-default
- %span
- To register a new Runner you should enter the following registration
- token.
- With this token the Runner will request a unique Runner token and use
- that for future communication.
- %br
- Registration token is
- %code#runners-token= current_application_settings.runners_registration_token
-
- .bs-callout.clearfix
- .pull-left
- %p
- You can reset runners registration token by pressing a button below.
- .prepend-top-10
- = button_to "Reset runners registration token", reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset registration token?' }
-
.bs-callout
%p
A 'Runner' is a process which runs a job.
@@ -46,6 +26,19 @@
%span.label.label-danger paused
\- Runner will not receive any new jobs
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset runners registration token by pressing a button below.
+ .prepend-top-10
+ = button_to _("Reset runners registration token"), reset_runners_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: _("Are you sure you want to reset registration token?") }
+
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: current_application_settings.runners_registration_token,
+ type: 'shared' }
+
.append-bottom-20.clearfix
.pull-left
= form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
new file mode 100644
index 00000000000..b75dab0acc5
--- /dev/null
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -0,0 +1,16 @@
+- link = link_to _("GitLab Runner section"), 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'
+.bs-callout.help-callout
+ %h4= _("How to setup a #{type} Runner for a new project")
+
+ %ol
+ %li
+ = _("Install a Runner compatible with GitLab CI")
+ = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe
+ %li
+ = _("Specify the following URL during the Runner setup:")
+ %code= root_url(only_path: false)
+ %li
+ = _("Use the following registration token during setup:")
+ %code#registration_token= registration_token
+ %li
+ = _("Start the Runner!")
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index e80d10dc8f1..bfd7dd25a7d 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,6 +7,6 @@
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset
+ %fieldset.prepend-top-10
= check_box_tag :remember_me
- = label_tag :remember_me, 'Remember Me'
+ = label_tag :remember_me, 'Remember me'
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
deleted file mode 100644
index b1694c919d0..00000000000
--- a/app/views/groups/_shared_projects.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'shared/projects/list', projects: projects, stars: false, skip_namespace: false
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index 4697d91724b..60940dba475 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -6,8 +6,8 @@
%h1.title
= link_to root_path, title: 'Dashboard' do
= brand_header_logo
- %span.hidden-xs
- GitLab
+ %span.logo-text.hidden-xs
+ = render 'shared/logo_type.svg'
- if current_user
= render "layouts/nav/new_dashboard"
@@ -81,6 +81,6 @@
%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', style: 'display: none;')
+ = icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/help.html.haml b/app/views/layouts/help.html.haml
index 224b24befbe..78927bfffcd 100644
--- a/app/views/layouts/help.html.haml
+++ b/app/views/layouts/help.html.haml
@@ -1,3 +1,4 @@
+- @breadcrumb_title = "Help"
- page_title "Help"
- header_title "Help", help_path
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml
index 239e6b949e2..6bbd569583e 100644
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml
@@ -47,6 +47,10 @@
= 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
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 21f175291fa..00395b222e4 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -75,10 +75,10 @@
Registry
- if project_nav_tab? :issues
- = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
+ = 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
- - if @project.default_issues_tracker?
+ - if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
Issues
@@ -113,7 +113,7 @@
Milestones
- if project_nav_tab? :merge_requests
- = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
+ = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 424905ea890..26d9640e98a 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -43,6 +43,10 @@
= 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
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index fb90bb4b472..924cd2e9681 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -23,16 +23,16 @@
Registry
- if project_nav_tab? :issues
- = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
+ = 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.default_issues_tracker?
+ - if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- - controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker?
+ - 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
diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml
new file mode 100644
index 00000000000..4b9350c4e88
--- /dev/null
+++ b/app/views/notify/new_gpg_key_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ Hi #{@user.name}!
+%p
+ A new GPG key was added to your account:
+%p
+ Fingerprint:
+ %code= @gpg_key.fingerprint
+%p
+ If this key was added in error, you can remove it under
+ = link_to "GPG Keys", profile_gpg_keys_url
diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb
new file mode 100644
index 00000000000..80b5a1fd7ff
--- /dev/null
+++ b/app/views/notify/new_gpg_key_email.text.erb
@@ -0,0 +1,7 @@
+Hi <%= @user.name %>!
+
+A new GPG key was added to your account:
+
+Fingerprint: <%= @gpg_key.fingerprint %>
+
+If this key was added in error, you can remove it at <%= profile_gpg_keys_url %>
diff --git a/app/views/profiles/gpg_keys/_email_with_badge.html.haml b/app/views/profiles/gpg_keys/_email_with_badge.html.haml
new file mode 100644
index 00000000000..5f7844584e1
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_email_with_badge.html.haml
@@ -0,0 +1,8 @@
+- css_classes = %w(label label-verification-status)
+- css_classes << (verified ? 'verified': 'unverified')
+- text = verified ? 'Verified' : 'Unverified'
+
+.gpg-email-badge
+ .gpg-email-badge-email= email
+ %div{ class: css_classes }
+ = text
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
new file mode 100644
index 00000000000..3fcf563d970
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -0,0 +1,10 @@
+%div
+ = form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
+ = form_errors(@gpg_key)
+
+ .form-group
+ = f.label :key, class: 'label-light'
+ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
+
+ .prepend-top-default
+ = f.submit 'Add key', class: "btn btn-create"
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
new file mode 100644
index 00000000000..b04981f90e3
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -0,0 +1,18 @@
+%li.key-list-item
+ .pull-left.append-right-10
+ = icon 'key', class: "settings-list-icon hidden-xs"
+ .key-list-item-info
+ - key.emails_with_verified_status.map do |email, verified|
+ = render partial: 'email_with_badge', locals: { email: email, verified: verified }
+
+ .description
+ %code= key.fingerprint
+ .pull-right
+ %span.key-created-at
+ created #{time_ago_with_tooltip(key.created_at)}
+ = link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Remove
+ = icon('trash')
+ = link_to revoke_profile_gpg_key_path(key), data: { confirm: 'Are you sure? All commits that were signed with this GPG key will be unverified.' }, method: :put, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Revoke
+ Revoke
diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml
new file mode 100644
index 00000000000..cabb92c5a24
--- /dev/null
+++ b/app/views/profiles/gpg_keys/_key_table.html.haml
@@ -0,0 +1,11 @@
+- is_admin = local_assigns.fetch(:admin, false)
+
+- if @gpg_keys.any?
+ %ul.well-list
+ = render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
+- else
+ %p.settings-message.text-center
+ - if is_admin
+ There are no GPG keys associated with this account.
+ - else
+ There are no GPG keys with access to your account.
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
new file mode 100644
index 00000000000..8331daeeb75
--- /dev/null
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -0,0 +1,21 @@
+- page_title "GPG Keys"
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ GPG keys allow you to verify signed commits.
+ .col-lg-9
+ %h5.prepend-top-0
+ Add a GPG key
+ %p.profile-settings-content
+ Before you can add a GPG key you need to
+ = link_to 'generate it.', help_page_path('workflow/gpg_signed_commits/index.md')
+ = render 'form'
+ %hr
+ %h5
+ Your GPG keys (#{@gpg_keys.count})
+ .append-bottom-default
+ = render 'key_table'
diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml
new file mode 100644
index 00000000000..4f3698f91e6
--- /dev/null
+++ b/app/views/projects/_deletion_failed.html.haml
@@ -0,0 +1,6 @@
+- project = local_assigns.fetch(:project)
+- return unless project.delete_error.present?
+
+.project-deletion-failed-message.alert.alert-warning
+ This project was scheduled for deletion, but failed with the following message:
+ = project.delete_error
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
new file mode 100644
index 00000000000..f47d84ef755
--- /dev/null
+++ b/app/views/projects/_flash_messages.html.haml
@@ -0,0 +1,8 @@
+- project = local_assigns.fetch(:project)
+- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
+
+= content_for flash_message_container do
+ = render partial: 'deletion_failed', locals: { project: project }
+ - if current_user && can?(current_user, :download_code, project)
+ = render 'shared/no_ssh'
+ = render 'shared/no_password'
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index f11afe8fc22..c7359d873d9 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -21,8 +21,8 @@
.commit
= author_avatar(commit, size: 36)
.commit-row-title
- %strong
- = link_to_gfm truncate(commit.title, length: 35), project_commit_path(@project, commit.id), class: "cdark"
+ %span.item-title.str-truncated-100
+ = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
.pull-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
index 640d59b3174..5fd22a59217 100644
--- a/app/views/projects/blob/viewers/_image.html.haml
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -1,2 +1,2 @@
.file-content.image_file
- %img{ src: blob_raw_url, alt: viewer.blob.name }
+ = image_tag(blob_raw_url, alt: viewer.blob.name)
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index e248676be0d..c82ae35a685 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -2,7 +2,7 @@
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
- %span.starred= _('Unstar')
+ %span.starred= _('Unstar')
- else
= icon('star-o')
%span= s_('StarProject|Star')
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
new file mode 100644
index 00000000000..22674b671c9
--- /dev/null
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -0,0 +1,3 @@
+- if commit.has_signature?
+ %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 45109f2c58b..419fbe99af8 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,5 +1,6 @@
.page-content-header
.header-main-content
+ = render partial: 'signature', object: @commit.signature
%strong
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml
new file mode 100644
index 00000000000..3a73aae9d95
--- /dev/null
+++ b/app/views/projects/commit/_invalid_signature_badge.html.haml
@@ -0,0 +1,9 @@
+- title = capture do
+ .gpg-popover-icon.invalid
+ = render 'shared/icons/icon_status_notfound_borderless.svg'
+ %div
+ This commit was signed with an <strong>unverified</strong> signature.
+
+- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
new file mode 100644
index 00000000000..60fa52557ef
--- /dev/null
+++ b/app/views/projects/commit/_signature.html.haml
@@ -0,0 +1,5 @@
+- if signature
+ - if signature.valid_signature?
+ = render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
+ - else
+ = render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
new file mode 100644
index 00000000000..66f00eb5507
--- /dev/null
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -0,0 +1,18 @@
+- css_classes = commit_signature_badge_classes(css_classes)
+
+- title = capture do
+ .gpg-popover-status
+ = title
+
+- content = capture do
+ .clearfix
+ = content
+
+ GPG Key ID:
+ %span.monospace= signature.gpg_key_primary_keyid
+
+
+ = link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+
+%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
+ = label
diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml
new file mode 100644
index 00000000000..db1a41bbf64
--- /dev/null
+++ b/app/views/projects/commit/_valid_signature_badge.html.haml
@@ -0,0 +1,32 @@
+- title = capture do
+ .gpg-popover-icon.valid
+ = render 'shared/icons/icon_status_success_borderless.svg'
+ %div
+ This commit was signed with a <strong>verified</strong> signature.
+
+- content = capture do
+ - gpg_key = signature.gpg_key
+ - user = gpg_key&.user
+ - user_name = signature.gpg_key_user_name
+ - user_email = signature.gpg_key_user_email
+
+ - if user
+ = link_to user_path(user), class: 'gpg-popover-user-link' do
+ %div
+ = user_avatar_without_link(user: user, size: 32)
+
+ %div
+ %strong= gpg_key.user.name
+ %div @#{gpg_key.user.username}
+ - else
+ = mail_to user_email do
+ %div
+ = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
+
+ %div
+ %strong= user_name
+ %div= user_email
+
+- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1033bad0d49..12b73ecdf13 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,7 +9,7 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
+ %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
@@ -36,9 +36,15 @@
#{ commit_text.html_safe }
- .commit-actions.flex-row.hidden-xs
+ .commit-actions.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
+
+ - if request.xhr?
+ = render partial: 'projects/commit/signature', object: commit.signature
+ - else
+ = render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
+
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index c764e35dd2a..d14897428d0 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -7,7 +7,7 @@
%span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
%li.commits-row{ data: { day: day } }
- %ul.content-list.commit-list
+ %ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
- if hidden > 0
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 844ebb65148..bd2d900997e 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -29,7 +29,7 @@
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form') do
+ = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index 33d3dcbeafa..aa004a739d7 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -8,7 +8,7 @@
.image
%span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
- %img{ src: blob_raw_path, alt: diff_file.file_path }
+ = image_tag(blob_raw_path, alt: diff_file.file_path)
%p.image-info= number_to_human_size(blob.size)
- else
.image
@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
.swipe-wrap
.frame.added
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ = image_tag(old_blob_raw_path, alt: diff_file.old_path)
.frame.added
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ = image_tag(blob_raw_path, alt: diff_file.new_path)
.controls
.transparent
.drag-track
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 0f132a68ce1..d17709380d5 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,10 +1,6 @@
- @no_container = true
-- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
-= content_for flash_message_container do
- - if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
+= render partial: 'flash_messages', locals: { project: @project }
= render "projects/head"
= render "home_panel"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index d02ea5cccc3..4b9da02c6b8 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,6 +1,7 @@
- @no_container = true
- page_title "Labels"
- hide_class = ''
+- can_admin_label = can?(current_user, :admin_label, @project)
- if show_new_nav? && can?(current_user, :admin_label, @project)
- content_for :breadcrumbs_extra do
@@ -12,15 +13,17 @@
%div{ class: container_class }
.top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+ Labels can be applied to issues and merge requests.
+ - if can_admin_label
+ Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
- - if can?(current_user, :admin_label, @project)
+ - if can_admin_label
+ .nav-controls{ class: ("visible-xs" if show_new_nav?) }
= link_to new_project_label_path(@project), class: "btn btn-new" do
New label
.labels
- - if can?(current_user, :admin_label, @project)
+ - if can_admin_label
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
@@ -33,7 +36,7 @@
- if @labels.present?
.other-labels
- - if can?(current_user, :admin_label, @project)
+ - if can_admin_label
%h5{ class: ('hide' if hide) } Other Labels
%ul.content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 766cb272bec..917ec7fdbda 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('how_to_merge')
+
#modal_merge_info.modal
.modal-dialog
.modal-content
@@ -50,14 +53,3 @@
= succeed '.' do
You can also checkout merge requests locally by
= link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
-
-:javascript
- $(function(){
- var modal = $('#modal_merge_info').modal({modal: true, show:false});
- $('.how_to_merge_link').bind("click", function(){
- modal.show();
- });
- $('.modal-header .close').bind("click", function(){
- modal.hide();
- })
- })
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 4e5aae496b1..8958b2cf5e1 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .merge-request-branches.row
+ .merge-request-branches.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6
.panel.panel-default.panel-new-merge-request
.panel-heading
@@ -66,10 +66,3 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
-
-:javascript
- new Compare({
- targetProjectUrl: "#{project_new_merge_request_update_branches_path(@source_project)}",
- sourceBranchUrl: "#{project_new_merge_request_branch_from_path(@source_project)}",
- targetBranchUrl: "#{project_new_merge_request_branch_to_path(@source_project)}"
- });
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index c72dd1d8e29..4b5fa28078a 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -17,7 +17,7 @@
= f.hidden_field :target_project_id
= f.hidden_field :target_branch
-.mr-compare.merge-request
+.mr-compare.merge-request.js-merge-request-new-submit{ 'data-mr-submit-action': "#{j params[:tab].presence || 'new'}" }
- if @commits.empty?
.commits-empty
%h4
@@ -50,8 +50,3 @@
.mr-loading-status
= spinner
-
-:javascript
- var merge_request = new MergeRequest({
- action: "#{j params[:tab].presence || 'new'}",
- });
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index bfeb746ee83..c020e7db380 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -4,7 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
-- unless @project.default_issues_tracker?
+- unless @project.issues_enabled?
= content_for :sub_nav do
= render "projects/merge_requests/head"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 2efc1d68190..ea6cd16c7ad 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -3,10 +3,10 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
- = page_specific_javascript_bundle_tag('diff_notes')
+ = webpack_bundle_tag('common_vue')
+ = webpack_bundle_tag('diff_notes')
-.merge-request{ 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
+.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -15,13 +15,13 @@
- if @merge_request.source_branch_exists?
= render "projects/merge_requests/how_to_merge"
+ -# haml-lint:disable InlineJavaScript
:javascript
window.gl.mrWidgetData = #{serialize_issuable(@merge_request)}
#js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container
@@ -88,10 +88,3 @@
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- if @merge_request.can_be_cherry_picked?
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
-
-:javascript
- $(function () {
- window.mergeRequest = new MergeRequest({
- action: "#{j params[:tab].presence || 'show'}",
- });
- });
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index a89387bc8f1..e0b29b0c2e1 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?
+- if show_new_nav? && 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
+ .nav-controls{ class: ("nav-controls-new-nav" if show_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', title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index a2d7a21d5f6..87cc23fc649 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -72,7 +72,7 @@
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'Fogbugz')
+ = icon('bug', text: 'FogBugz')
%div
- if gitea_import_enabled?
= link_to new_import_gitea_url, class: 'btn import_gitea' do
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 97c0407a01d..7343d6e039c 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -4,7 +4,7 @@
= pipeline_schedule.description
%td.branch-name-cell
= icon('code-fork')
- - if pipeline_schedule.ref
+ - if pipeline_schedule.ref.present?
= link_to pipeline_schedule.ref, project_ref_path(@project, pipeline_schedule.ref), class: "ref-name"
%td
- if pipeline_schedule.last_pipeline
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 5c00bb6883c..2a0704bc7af 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-branches-list
+.panel.panel-default.protected-branches-list.js-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
@@ -23,6 +23,8 @@
- if can_admin_project
%th
%tbody
+ %tr
+ %td.flash-container{ colspan: 5 }
= yield
= paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index b619fa57e05..9f0c4f3b3a8 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 6e3cd4ada71..3f42ae58438 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,4 +1,4 @@
-.panel.panel-default.protected-tags-list
+.panel.panel-default.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index f8835454140..28ccbf7eb15 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,21 +1,8 @@
%h3 Specific Runners
-.bs-callout.help-callout
- %h4 How to setup a specific Runner for a new project
-
- %ol
- %li
- Install a Runner compatible with GitLab CI
- (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
- %li
- Specify the following URL during the Runner setup:
- %code= root_url(only_path: false)
- %li
- Use the following registration token during setup:
- %code= @project.runners_token
- %li
- Start the Runner!
-
+= render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @project.runners_token,
+ type: 'specific' }
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index ac98a5a5b50..18ef1c93c3c 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- breadcrumb_title "Project"
- @content_class = "limit-container-width" unless fluid_layout
-- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
@@ -10,10 +9,7 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo'
-= content_for flash_message_container do
- - if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
+= render partial: 'flash_messages', locals: { project: @project }
= render "projects/head"
= render "projects/last_push"
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index fc6b7a33943..adb8d5aaecb 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -4,6 +4,8 @@
= form_errors(@page)
= f.hidden_field :title, value: @page.title
+ - if @page.persisted?
+ = f.hidden_field :last_commit_sha, value: @page.last_commit_sha
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index df0ec14eb3b..8fd60216536 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,6 +1,12 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
- page_title "Edit", @page.title.capitalize, "Wiki"
+- if @conflict
+ .alert.alert-danger
+ Someone edited the page the same time you did. Please check out
+ = link_to "the page", project_wiki_path(@project, @page), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs.
+
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 75704eda361..b4843eafdb7 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -20,11 +20,3 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-btn
= clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard")
-
-:javascript
- $('ul.clone-options-dropdown a').on('click',function(e){
- e.preventDefault();
- var $this = $(this);
- $('a.clone-dropdown-btn span').text($this.text());
- $('#project_clone').val($this.attr('href'));
- });
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 2f776a17f45..8ded7440de3 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -76,11 +76,3 @@
= link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
-
- - if current_user
- - if can_subscribe_to_label_in_different_levels?(label)
- :javascript
- new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription');
- - else
- :javascript
- new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_logo_type.svg b/app/views/shared/_logo_type.svg
new file mode 100644
index 00000000000..cb07e2634a9
--- /dev/null
+++ b/app/views/shared/_logo_type.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 617 169"><path d="M315.26 2.97h-21.8l.1 162.5h88.3v-20.1h-66.5l-.1-142.4M465.89 136.95c-5.5 5.7-14.6 11.4-27 11.4-16.6 0-23.3-8.2-23.3-18.9 0-16.1 11.2-23.8 35-23.8 4.5 0 11.7.5 15.4 1.2v30.1h-.1m-22.6-98.5c-17.6 0-33.8 6.2-46.4 16.7l7.7 13.4c8.9-5.2 19.8-10.4 35.5-10.4 17.9 0 25.8 9.2 25.8 24.6v7.9c-3.5-.7-10.7-1.2-15.1-1.2-38.2 0-57.6 13.4-57.6 41.4 0 25.1 15.4 37.7 38.7 37.7 15.7 0 30.8-7.2 36-18.9l4 15.9h15.4v-83.2c-.1-26.3-11.5-43.9-44-43.9M557.63 149.1c-8.2 0-15.4-1-20.8-3.5V70.5c7.4-6.2 16.6-10.7 28.3-10.7 21.1 0 29.2 14.9 29.2 39 0 34.2-13.1 50.3-36.7 50.3m9.2-110.6c-19.5 0-30 13.3-30 13.3v-21l-.1-27.8h-21.3l.1 158.5c10.7 4.5 25.3 6.9 41.2 6.9 40.7 0 60.3-26 60.3-70.9-.1-35.5-18.2-59-50.2-59M77.9 20.6c19.3 0 31.8 6.4 39.9 12.9l9.4-16.3C114.5 6 97.3 0 78.9 0 32.5 0 0 28.3 0 85.4c0 59.8 35.1 83.1 75.2 83.1 20.1 0 37.2-4.7 48.4-9.4l-.5-63.9V75.1H63.6v20.1h38l.5 48.5c-5 2.5-13.6 4.5-25.3 4.5-32.2 0-53.8-20.3-53.8-63-.1-43.5 22.2-64.6 54.9-64.6M231.43 2.95h-21.3l.1 27.3v94.3c0 26.3 11.4 43.9 43.9 43.9 4.5 0 8.9-.4 13.1-1.2v-19.1c-3.1.5-6.4.7-9.9.7-17.9 0-25.8-9.2-25.8-24.6v-65h35.7v-17.8h-35.7l-.1-38.5M155.96 165.47h21.3v-124h-21.3v124M155.96 24.37h21.3V3.07h-21.3v21.3"/></svg>
diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml
index 4211ec6351d..e7355ae2eea 100644
--- a/app/views/shared/_mr_head.html.haml
+++ b/app/views/shared/_mr_head.html.haml
@@ -1,4 +1,4 @@
-- if @project.default_issues_tracker?
+- if @project.issues_enabled?
= render "projects/issues/head"
- else
= render "projects/merge_requests/head"
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index c1acee1a211..5f3cdaefd54 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if @projects.any?
.project-item-select-holder
- = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %a.btn.btn-new.new-project-item-select-button{ data: { relative_path: local_assigns[:path] } }
+ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
+ %a.btn.btn-new.new-project-item-select-button
= local_assigns[:label]
= icon('caret-down')
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index b20055a564e..e415ec64c38 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -23,18 +23,3 @@
.prepend-top-default
= f.submit "Create #{type} token", class: "btn btn-create"
-
-:javascript
- var $dateField = $('.datepicker');
- var date = $dateField.val();
-
- new Pikaday({
- field: $dateField.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- minDate: new Date(),
- container: $dateField.parent().get(0),
- onSelect: function(dateText) {
- $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
- }
- });
diff --git a/app/views/shared/icons/_icon_clone.svg b/app/views/shared/icons/_icon_clone.svg
new file mode 100644
index 00000000000..ccc897aa98f
--- /dev/null
+++ b/app/views/shared/icons/_icon_clone.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14">
+<path d="M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"></path>
+</svg>
diff --git a/app/views/shared/icons/_icon_status_notfound_borderless.svg b/app/views/shared/icons/_icon_status_notfound_borderless.svg
new file mode 100644
index 00000000000..e58bd264ef8
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_notfound_borderless.svg
@@ -0,0 +1 @@
+<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg>
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 964fe5220f7..0d507cc7a6e 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -1,9 +1,9 @@
- type = local_assigns.fetch(:type)
-%aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" }
+%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
- .block
+ .block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.pull-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
= button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 2cabbc8c560..c4ed7f6e750 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -36,13 +36,3 @@
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels
= render 'shared/labels_row', labels: @labels
-
-:javascript
- new LabelsSelect();
- new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- gl.utils.visitUrl(this.action + '&' + $(this).serialize());
- });
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index db407363a09..8a71819aa8e 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -16,5 +16,3 @@
.hide-collapsed.participants-more
%a.js-participants-more{ href: "#", data: { original_text: "+ #{participants_size - 7} more", less_text: "- show less" } }
+ #{participants_extra} more
-:javascript
- IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 6f0b7600698..3428d6e0445 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -108,12 +108,3 @@
#js-add-issues-btn.prepend-left-10
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
-
-- unless type === :boards_modal
- :javascript
- $(document).off('page:restore').on('page:restore', function (event) {
- if (gl.FilteredSearchManager) {
- const filteredSearchManager = new gl.FilteredSearchManager();
- filteredSearchManager.setup();
- }
- });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ecbaa901792..b08267357e5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -138,17 +138,4 @@
= project_ref
= clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
- :javascript
- gl.sidebarOptions = {
- endpoint: "#{issuable_json_path(issuable)}?basic=true",
- editable: #{can_edit_issuable ? true : false},
- currentUser: #{current_user.to_json(only: [:username, :id, :name], methods: :avatar_url)},
- rootPath: "#{root_path}"
- };
-
- new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
- new LabelsSelect();
- new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
- gl.Subscription.bindAll('.subscription');
- new gl.DueDateSelectors();
- window.sidebar = new Sidebar();
+ %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
index 549d2e2f61e..1615871385e 100644
--- a/app/views/shared/milestones/_participants_tab.html.haml
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -4,5 +4,5 @@
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
%strong= truncate(user.name, length: 40)
- %br
- %small.cgray= user.username
+ %div
+ %small.cgray= user.username
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index f0fcc414756..eae04c9bbb8 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -22,5 +22,4 @@
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
to comment
-:javascript
- var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}", #{autocomplete})
+%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index ac3701233ad..dfea8b40bd8 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,4 +1,3 @@
-- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
@@ -8,7 +7,4 @@
%li
.nothing-here-block Nothing here.
- = paginate @snippets, theme: 'gitlab', remote: remote
-
-:javascript
- gl.SnippetsList();
+ = paginate @snippets, theme: 'gitlab'
diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml
deleted file mode 100644
index 57b8845c55d..00000000000
--- a/app/views/users/calendar.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.clearfix.calendar
- .js-contrib-calendar
- .calendar-hint
- Summary of issues, merge requests, push events, and comments
-:javascript
- new Calendar(
- #{@activity_dates.to_json},
- '#{user_calendar_activities_path}'
- );
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 919ba5d15d3..a449706c567 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -2,9 +2,6 @@
- @hide_breadcrumbs = true
- page_title @user.name
- page_description @user.bio
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_d3')
- = page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
@@ -107,7 +104,7 @@
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
- .user-calendar{ data: { href: user_calendar_path } }
+ .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } }
%h4.center.light
%i.fa.fa-spinner.fa-spin
.user-calendar-activities
@@ -131,10 +128,3 @@
.loading-status
= spinner
-
-:javascript
- var userProfile;
-
- userProfile = new gl.User({
- action: "#{controller.action_name}"
- });
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
new file mode 100644
index 00000000000..4f47717ff69
--- /dev/null
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -0,0 +1,16 @@
+class CreateGpgSignatureWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(commit_sha, project_id)
+ project = Project.find_by(id: project_id)
+
+ return unless project
+
+ commit = project.commit(commit_sha)
+
+ return unless commit
+
+ commit.signature
+ end
+end
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
new file mode 100644
index 00000000000..db6b1ea8e8d
--- /dev/null
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -0,0 +1,12 @@
+class InvalidGpgSignatureUpdateWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(gpg_key_id)
+ gpg_key = GpgKey.find_by(id: gpg_key_id)
+
+ return unless gpg_key
+
+ Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run
+ end
+end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 7b485b3363c..d7087f20dfc 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -6,15 +6,12 @@ class PipelineScheduleWorker
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
begin
- unless schedule.runnable_by_owner?
- schedule.deactivate!
- next
- end
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
+ pipeline = Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
.execute(:schedule, save_on_errors: false, schedule: schedule)
+
+ schedule.deactivate! unless pipeline.persisted?
rescue => e
Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}"
ensure
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index b462327490e..a9188b78460 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -3,14 +3,11 @@ class ProjectDestroyWorker
include DedicatedSidekiqQueue
def perform(project_id, user_id, params)
- begin
- project = Project.unscoped.find(project_id)
- rescue ActiveRecord::RecordNotFound
- return
- end
-
+ project = Project.find(project_id)
user = User.find(user_id)
::Projects::DestroyService.new(project, user, params.symbolize_keys).execute
+ rescue ActiveRecord::RecordNotFound => error
+ logger.error("Failed to delete project (#{project_id}): #{error.message}")
end
end