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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-18 09:11:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-18 09:11:52 +0300
commit4d16568658ac6fb0003b407e07a76c11e607f44f (patch)
tree9084e7660f101d2cd70568f293257678ac5f2ef5
parentf5410eefec8642bed6e7e3051319c52d7cbfb101 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api/user_api.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js594
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js65
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js20
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js27
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js70
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js21
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js3
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/design_navigation.vue14
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue5
-rw-r--r--app/assets/javascripts/diffs/components/app.vue38
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_navigator.vue13
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue12
-rw-r--r--app/controllers/graphql_controller.rb1
-rw-r--r--app/graphql/gitlab_schema.rb1
-rw-r--r--app/graphql/mutations/base_mutation.rb24
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb13
-rw-r--r--app/graphql/resolvers/alert_management/http_integrations_resolver.rb2
-rw-r--r--app/graphql/resolvers/alert_management/integrations_resolver.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb18
-rw-r--r--app/graphql/resolvers/board_lists_resolver.rb18
-rw-r--r--app/graphql/resolvers/board_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/manual_authorization.rb11
-rw-r--r--app/graphql/resolvers/group_merge_requests_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb2
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects/services_resolver.rb12
-rw-r--r--app/graphql/resolvers/snippets/blobs_resolver.rb3
-rw-r--r--app/graphql/resolvers/user_merge_requests_resolver_base.rb2
-rw-r--r--app/graphql/types/base_enum.rb12
-rw-r--r--app/graphql/types/base_field.rb37
-rw-r--r--app/graphql/types/base_interface.rb6
-rw-r--r--app/graphql/types/base_object.rb8
-rw-r--r--app/graphql/types/base_union.rb3
-rw-r--r--app/helpers/page_layout_helper.rb11
-rw-r--r--app/presenters/packages/detail/package_presenter.rb1
-rw-r--r--changelogs/unreleased/262086-user-availability-allow-users-to-schedule-un-setting-of-their-stat.yml5
-rw-r--r--changelogs/unreleased/56716-remove-commit-message-package-ui.yml5
-rw-r--r--config/feature_flags/development/diff_line_syntax_highlighting.yml8
-rw-r--r--config/initializers/graphql.rb2
-rw-r--r--doc/administration/auth/ldap/ldap-troubleshooting.md2
-rw-r--r--doc/administration/geo/replication/security_review.md4
-rw-r--r--doc/administration/gitaly/praefect.md105
-rw-r--r--doc/administration/troubleshooting/postgresql.md2
-rw-r--r--doc/api/graphql/getting_started.md2
-rw-r--r--doc/ci/caching/index.md3
-rw-r--r--doc/ci/multi_project_pipelines.md5
-rw-r--r--doc/development/fe_guide/vue.md2
-rw-r--r--doc/development/i18n/translation.md2
-rw-r--r--doc/development/pipelines.md11
-rw-r--r--doc/development/usage_ping/dictionary.md24
-rw-r--r--doc/install/pivotal/index.md2
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_10.pngbin0 -> 51704 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_6.pngbin62882 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md9
-rw-r--r--doc/user/profile/index.md4
-rw-r--r--doc/user/project/clusters/serverless/aws.md2
-rw-r--r--doc/user/project/integrations/jira_integrations.md4
-rw-r--r--lefthook.yml6
-rw-r--r--lib/gitlab/diff/highlight.rb36
-rw-r--r--lib/gitlab/diff/highlight_cache.rb6
-rw-r--r--lib/gitlab/diff/line.rb10
-rw-r--r--lib/gitlab/graphql/authorize.rb15
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb147
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb44
-rw-r--r--lib/gitlab/graphql/authorize/connection_filter_extension.rb63
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb21
-rw-r--r--lib/gitlab/graphql/authorize/object_authorization.rb31
-rw-r--r--lib/gitlab/highlight.rb10
-rw-r--r--lib/gitlab/usage_data_counters/known_events/epic_events.yml6
-rw-r--r--lib/rouge/formatters/html_gitlab.rb5
-rw-r--r--locale/gitlab.pot56
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js13
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js58
-rw-r--r--spec/graphql/features/authorization_spec.rb98
-rw-r--r--spec/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb66
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb10
-rw-r--r--spec/graphql/resolvers/concerns/looks_ahead_spec.rb9
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb2
-rw-r--r--spec/graphql/types/alert_management/prometheus_integration_type_spec.rb24
-rw-r--r--spec/graphql/types/base_object_spec.rb434
-rw-r--r--spec/helpers/page_layout_helper_spec.rb52
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/line_spec.rb23
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb253
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb77
-rw-r--r--spec/lib/gitlab/highlight_spec.rb15
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/lib/rouge/formatters/html_gitlab_spec.rb40
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb37
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/C++.gitignore0
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/Java.gitignore0
99 files changed, 2164 insertions, 888 deletions
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index 5efc7063efa..27901120c53 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -55,12 +55,13 @@ export function getUserProjects(userId, query, options, callback) {
.catch(() => flash(__('Something went wrong while fetching projects')));
}
-export function updateUserStatus({ emoji, message, availability }) {
+export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) {
const url = buildApiUrl(USER_POST_STATUS_PATH);
return axios.put(url, {
emoji,
message,
availability,
+ clear_status_after: clearStatusAfter,
});
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index a8fe00d26e6..cb5fb5b4bed 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -1,6 +1,6 @@
import { memoize } from 'lodash';
import AccessorUtilities from '~/lib/utils/accessor';
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
const isCustomizable = (command) =>
'customizable' in command ? Boolean(command.customizable) : true;
@@ -33,42 +33,608 @@ export const getCustomizations = memoize(() => {
});
// All available commands
+export const TOGGLE_KEYBOARD_SHORTCUTS_DIALOG = {
+ id: 'globalShortcuts.toggleKeyboardShortcutsDialog',
+ description: __('Toggle keyboard shortcuts help dialog'),
+ defaultKeys: ['?'],
+};
+
+export const GO_TO_YOUR_PROJECTS = {
+ id: 'globalShortcuts.goToYourProjects',
+ description: __('Go to your projects'),
+ defaultKeys: ['shift+p'],
+};
+
+export const GO_TO_YOUR_GROUPS = {
+ id: 'globalShortcuts.goToYourGroups',
+ description: __('Go to your groups'),
+ defaultKeys: ['shift+g'],
+};
+
+export const GO_TO_ACTIVITY_FEED = {
+ id: 'globalShortcuts.goToActivityFeed',
+ description: __('Go to the activity feed'),
+ defaultKeys: ['shift+a'],
+};
+
+export const GO_TO_MILESTONE_LIST = {
+ id: 'globalShortcuts.goToMilestoneList',
+ description: __('Go to the milestone list'),
+ defaultKeys: ['shift+l'],
+};
+
+export const GO_TO_YOUR_SNIPPETS = {
+ id: 'globalShortcuts.goToYourSnippets',
+ description: __('Go to your snippets'),
+ defaultKeys: ['shift+s'],
+};
+
+export const START_SEARCH = {
+ id: 'globalShortcuts.startSearch',
+ description: __('Start search'),
+ defaultKeys: ['s', '/'],
+};
+
+export const FOCUS_FILTER_BAR = {
+ id: 'globalShortcuts.focusFilterBar',
+ description: __('Focus filter bar'),
+ defaultKeys: ['f'],
+};
+
+export const GO_TO_YOUR_ISSUES = {
+ id: 'globalShortcuts.goToYourIssues',
+ description: __('Go to your issues'),
+ defaultKeys: ['shift+i'],
+};
+
+export const GO_TO_YOUR_MERGE_REQUESTS = {
+ id: 'globalShortcuts.goToYourMergeRequests',
+ description: __('Go to your merge requests'),
+ defaultKeys: ['shift+m'],
+};
+
+export const GO_TO_YOUR_TODO_LIST = {
+ id: 'globalShortcuts.goToYourTodoList',
+ description: __('Go to your To-Do list'),
+ defaultKeys: ['shift+t'],
+};
+
export const TOGGLE_PERFORMANCE_BAR = {
id: 'globalShortcuts.togglePerformanceBar',
- description: s__('KeyboardShortcuts|Toggle the Performance Bar'),
- // eslint-disable-next-line @gitlab/require-i18n-strings
- defaultKeys: ['p b'],
+ description: __('Toggle the Performance Bar'),
+ defaultKeys: ['p b'], // eslint-disable-line @gitlab/require-i18n-strings
};
export const TOGGLE_CANARY = {
id: 'globalShortcuts.toggleCanary',
- description: s__('KeyboardShortcuts|Toggle GitLab Next'),
- // eslint-disable-next-line @gitlab/require-i18n-strings
- defaultKeys: ['g x'],
+ description: __('Toggle GitLab Next'),
+ defaultKeys: ['g x'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const BOLD_TEXT = {
+ id: 'editing.boldText',
+ description: __('Bold text'),
+ defaultKeys: ['mod+b'],
+ customizable: false,
+};
+
+export const ITALIC_TEXT = {
+ id: 'editing.italicText',
+ description: __('Italic text'),
+ defaultKeys: ['mod+i'],
+ customizable: false,
+};
+
+export const LINK_TEXT = {
+ id: 'editing.linkText',
+ description: __('Link text'),
+ defaultKeys: ['mod+k'],
+ customizable: false,
+};
+
+export const TOGGLE_MARKDOWN_PREVIEW = {
+ id: 'editing.toggleMarkdownPreview',
+ description: __('Toggle Markdown preview'),
+ // Note: Ideally, keyboard shortcuts should be made cross-platform by using the special `mod` key
+ // instead of binding both `ctrl` and `command` versions of the shortcut.
+ // See https://docs.gitlab.com/ee/development/fe_guide/keyboard_shortcuts.html#make-cross-platform-shortcuts.
+ // However, this particular shortcut has been in place since before the `mod` key was available.
+ // We've chosen to leave this implemented as-is for the time being to avoid breaking people's workflows.
+ // See discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45308#note_527490548.
+ defaultKeys: ['ctrl+shift+p', 'command+shift+p'],
+};
+
+export const EDIT_RECENT_COMMENT = {
+ id: 'editing.editRecentComment',
+ description: __('Edit your most recent comment in a thread (from an empty textarea)'),
+ defaultKeys: ['up'],
+};
+
+export const EDIT_WIKI_PAGE = {
+ id: 'wiki.editWikiPage',
+ description: __('Edit wiki page'),
+ defaultKeys: ['e'],
+};
+
+export const REPO_GRAPH_SCROLL_LEFT = {
+ id: 'repositoryGraph.scrollLeft',
+ description: __('Scroll left'),
+ defaultKeys: ['left', 'h'],
+};
+
+export const REPO_GRAPH_SCROLL_RIGHT = {
+ id: 'repositoryGraph.scrollRight',
+ description: __('Scroll right'),
+ defaultKeys: ['right', 'l'],
+};
+
+export const REPO_GRAPH_SCROLL_UP = {
+ id: 'repositoryGraph.scrollUp',
+ description: __('Scroll up'),
+ defaultKeys: ['up', 'k'],
+};
+
+export const REPO_GRAPH_SCROLL_DOWN = {
+ id: 'repositoryGraph.scrollDown',
+ description: __('Scroll down'),
+ defaultKeys: ['down', 'j'],
+};
+
+export const REPO_GRAPH_SCROLL_TOP = {
+ id: 'repositoryGraph.scrollToTop',
+ description: __('Scroll to top'),
+ defaultKeys: ['shift+up', 'shift+k'],
+};
+
+export const REPO_GRAPH_SCROLL_BOTTOM = {
+ id: 'repositoryGraph.scrollToBottom',
+ description: __('Scroll to bottom'),
+ defaultKeys: ['shift+down', 'shift+j'],
+};
+
+export const GO_TO_PROJECT_OVERVIEW = {
+ id: 'project.goToOverview',
+ description: __("Go to the project's overview page"),
+ defaultKeys: ['g p'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_ACTIVITY_FEED = {
+ id: 'project.goToActivityFeed',
+ description: __("Go to the project's activity feed"),
+ defaultKeys: ['g v'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_RELEASES = {
+ id: 'project.goToReleases',
+ description: __('Go to releases'),
+ defaultKeys: ['g r'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_FILES = {
+ id: 'project.goToFiles',
+ description: __('Go to files'),
+ defaultKeys: ['g f'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_FIND_FILE = {
+ id: 'project.goToFindFile',
+ description: __('Go to find file'),
+ defaultKeys: ['t'],
+};
+
+export const GO_TO_PROJECT_COMMITS = {
+ id: 'project.goToCommits',
+ description: __('Go to commits'),
+ defaultKeys: ['g c'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_REPO_GRAPH = {
+ id: 'project.goToRepoGraph',
+ description: __('Go to repository graph'),
+ defaultKeys: ['g n'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_REPO_CHARTS = {
+ id: 'project.goToRepoCharts',
+ description: __('Go to repository charts'),
+ defaultKeys: ['g d'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_ISSUES = {
+ id: 'project.goToIssues',
+ description: __('Go to issues'),
+ defaultKeys: ['g i'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const NEW_ISSUE = {
+ id: 'project.newIssue',
+ description: __('New issue'),
+ defaultKeys: ['i'],
+};
+
+export const GO_TO_PROJECT_ISSUE_BOARDS = {
+ id: 'project.goToIssueBoards',
+ description: __('Go to issue boards'),
+ defaultKeys: ['g b'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_MERGE_REQUESTS = {
+ id: 'project.goToMergeRequests',
+ description: __('Go to merge requests'),
+ defaultKeys: ['g m'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_JOBS = {
+ id: 'project.goToJobs',
+ description: __('Go to jobs'),
+ defaultKeys: ['g j'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_METRICS = {
+ id: 'project.goToMetrics',
+ description: __('Go to metrics'),
+ defaultKeys: ['g l'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_ENVIRONMENTS = {
+ id: 'project.goToEnvironments',
+ description: __('Go to environments'),
+ defaultKeys: ['g e'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_KUBERNETES = {
+ id: 'project.goToKubernetes',
+ description: __('Go to kubernetes'),
+ defaultKeys: ['g k'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_SNIPPETS = {
+ id: 'project.goToSnippets',
+ description: __('Go to snippets'),
+ defaultKeys: ['g s'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const GO_TO_PROJECT_WIKI = {
+ id: 'project.goToWiki',
+ description: __('Go to wiki'),
+ defaultKeys: ['g w'], // eslint-disable-line @gitlab/require-i18n-strings
+};
+
+export const PROJECT_FILES_MOVE_SELECTION_UP = {
+ id: 'projectFiles.moveSelectionUp',
+ description: __('Move selection up'),
+ defaultKeys: ['up'],
+};
+
+export const PROJECT_FILES_MOVE_SELECTION_DOWN = {
+ id: 'projectFiles.moveSelectionDown',
+ description: __('Move selection down'),
+ defaultKeys: ['down'],
+};
+
+export const PROJECT_FILES_OPEN_SELECTION = {
+ id: 'projectFiles.openSelection',
+ description: __('Open Selection'),
+ defaultKeys: ['enter'],
+};
+
+export const PROJECT_FILES_GO_BACK = {
+ id: 'projectFiles.goBack',
+ description: __('Go back (while searching for files)'),
+ defaultKeys: ['esc'],
+};
+
+export const PROJECT_FILES_GO_TO_PERMALINK = {
+ id: 'projectFiles.goToFilePermalink',
+ description: __('Go to file permalink (while viewing a file)'),
+ defaultKeys: ['y'],
+};
+
+export const ISSUABLE_COMMENT_OR_REPLY = {
+ id: 'issuables.commentReply',
+ description: __('Comment/Reply (quoting selected text)'),
+ defaultKeys: ['r'],
+};
+
+export const ISSUABLE_EDIT_DESCRIPTION = {
+ id: 'issuables.editDescription',
+ description: __('Edit description'),
+ defaultKeys: ['e'],
+};
+
+export const ISSUABLE_CHANGE_LABEL = {
+ id: 'issuables.changeLabel',
+ description: __('Change label'),
+ defaultKeys: ['l'],
+};
+
+export const ISSUE_MR_CHANGE_ASSIGNEE = {
+ id: 'issuesMRs.changeAssignee',
+ description: __('Change assignee'),
+ defaultKeys: ['a'],
+};
+
+export const ISSUE_MR_CHANGE_MILESTONE = {
+ id: 'issuesMRs.changeMilestone',
+ description: __('Change milestone'),
+ defaultKeys: ['m'],
+};
+
+export const MR_NEXT_FILE_IN_DIFF = {
+ id: 'mergeRequests.nextFileInDiff',
+ description: __('Next file in diff'),
+ defaultKeys: [']', 'j'],
+};
+
+export const MR_PREVIOUS_FILE_IN_DIFF = {
+ id: 'mergeRequests.previousFileInDiff',
+ description: __('Previous file in diff'),
+ defaultKeys: ['[', 'k'],
+};
+
+export const MR_GO_TO_FILE = {
+ id: 'mergeRequests.goToFile',
+ description: __('Go to file'),
+ defaultKeys: ['t', 'mod+p'],
+ customizable: false,
+};
+
+export const MR_NEXT_UNRESOLVED_DISCUSSION = {
+ id: 'mergeRequests.nextUnresolvedDiscussion',
+ description: __('Next unresolved discussion'),
+ defaultKeys: ['n'],
+};
+
+export const MR_PREVIOUS_UNRESOLVED_DISCUSSION = {
+ id: 'mergeRequests.previousUnresolvedDiscussion',
+ description: __('Previous unresolved discussion'),
+ defaultKeys: ['p'],
+};
+
+export const MR_COPY_SOURCE_BRANCH_NAME = {
+ id: 'mergeRequests.copySourceBranchName',
+ description: __('Copy source branch name'),
+ defaultKeys: ['b'],
+};
+
+export const MR_COMMITS_NEXT_COMMIT = {
+ id: 'mergeRequestCommits.nextCommit',
+ description: __('Next commit'),
+ defaultKeys: ['c'],
+};
+
+export const MR_COMMITS_PREVIOUS_COMMIT = {
+ id: 'mergeRequestCommits.previousCommit',
+ description: __('Previous commit'),
+ defaultKeys: ['x'],
+};
+
+export const ISSUE_NEXT_DESIGN = {
+ id: 'issues.nextDesign',
+ description: __('Next design'),
+ defaultKeys: ['right'],
+};
+
+export const ISSUE_PREVIOUS_DESIGN = {
+ id: 'issues.previousDesign',
+ description: __('Previous design'),
+ defaultKeys: ['left'],
+};
+
+export const ISSUE_CLOSE_DESIGN = {
+ id: 'issues.closeDesign',
+ description: __('Close design'),
+ defaultKeys: ['esc'],
+};
+
+export const WEB_IDE_GO_TO_FILE = {
+ id: 'webIDE.goToFile',
+ description: __('Go to file'),
+ defaultKeys: ['mod+p'],
};
export const WEB_IDE_COMMIT = {
id: 'webIDE.commit',
- description: s__('KeyboardShortcuts|Commit (when editing commit message)'),
+ description: __('Commit (when editing commit message)'),
defaultKeys: ['mod+enter'],
customizable: false,
};
+export const METRICS_EXPAND_PANEL = {
+ id: 'metrics.expandPanel',
+ description: __('Expand panel'),
+ defaultKeys: ['e'],
+ customizable: false,
+};
+
+export const METRICS_VIEW_LOGS = {
+ id: 'metrics.viewLogs',
+ description: __('View logs'),
+ defaultKeys: ['l'],
+ customizable: false,
+};
+
+export const METRICS_DOWNLOAD_CSV = {
+ id: 'metrics.downloadCSV',
+ description: __('Download CSV'),
+ defaultKeys: ['d'],
+ customizable: false,
+};
+
+export const METRICS_COPY_LINK_TO_CHART = {
+ id: 'metrics.copyLinkToChart',
+ description: __('Copy link to chart'),
+ defaultKeys: ['c'],
+ customizable: false,
+};
+
+export const METRICS_SHOW_ALERTS = {
+ id: 'metrics.showAlerts',
+ description: __('Alerts'),
+ defaultKeys: ['a'],
+ customizable: false,
+};
+
// All keybinding groups
export const GLOBAL_SHORTCUTS_GROUP = {
id: 'globalShortcuts',
- name: s__('KeyboardShortcuts|Global Shortcuts'),
- keybindings: [TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY],
+ name: __('Global Shortcuts'),
+ keybindings: [
+ TOGGLE_KEYBOARD_SHORTCUTS_DIALOG,
+ GO_TO_YOUR_PROJECTS,
+ GO_TO_YOUR_GROUPS,
+ GO_TO_ACTIVITY_FEED,
+ GO_TO_MILESTONE_LIST,
+ GO_TO_YOUR_SNIPPETS,
+ START_SEARCH,
+ FOCUS_FILTER_BAR,
+ GO_TO_YOUR_ISSUES,
+ GO_TO_YOUR_MERGE_REQUESTS,
+ GO_TO_YOUR_TODO_LIST,
+ TOGGLE_PERFORMANCE_BAR,
+ ],
+};
+
+export const EDITING_SHORTCUTS_GROUP = {
+ id: 'editing',
+ name: __('Editing'),
+ keybindings: [BOLD_TEXT, ITALIC_TEXT, LINK_TEXT, TOGGLE_MARKDOWN_PREVIEW, EDIT_RECENT_COMMENT],
+};
+
+export const WIKI_SHORTCUTS_GROUP = {
+ id: 'wiki',
+ name: __('Wiki'),
+ keybindings: [EDIT_WIKI_PAGE],
+};
+
+export const REPOSITORY_GRAPH_SHORTCUTS_GROUP = {
+ id: 'repositoryGraph',
+ name: __('Repository Graph'),
+ keybindings: [
+ REPO_GRAPH_SCROLL_LEFT,
+ REPO_GRAPH_SCROLL_RIGHT,
+ REPO_GRAPH_SCROLL_UP,
+ REPO_GRAPH_SCROLL_DOWN,
+ REPO_GRAPH_SCROLL_TOP,
+ REPO_GRAPH_SCROLL_BOTTOM,
+ ],
+};
+
+export const PROJECT_SHORTCUTS_GROUP = {
+ id: 'project',
+ name: __('Project'),
+ keybindings: [
+ GO_TO_PROJECT_OVERVIEW,
+ GO_TO_PROJECT_ACTIVITY_FEED,
+ GO_TO_PROJECT_RELEASES,
+ GO_TO_PROJECT_FILES,
+ GO_TO_PROJECT_FIND_FILE,
+ GO_TO_PROJECT_COMMITS,
+ GO_TO_PROJECT_REPO_GRAPH,
+ GO_TO_PROJECT_REPO_CHARTS,
+ GO_TO_PROJECT_ISSUES,
+ NEW_ISSUE,
+ GO_TO_PROJECT_ISSUE_BOARDS,
+ GO_TO_PROJECT_MERGE_REQUESTS,
+ GO_TO_PROJECT_JOBS,
+ GO_TO_PROJECT_METRICS,
+ GO_TO_PROJECT_ENVIRONMENTS,
+ GO_TO_PROJECT_KUBERNETES,
+ GO_TO_PROJECT_SNIPPETS,
+ GO_TO_PROJECT_WIKI,
+ ],
+};
+
+export const PROJECT_FILES_SHORTCUTS_GROUP = {
+ id: 'projectFiles',
+ name: __('Project Files'),
+ keybindings: [
+ PROJECT_FILES_MOVE_SELECTION_UP,
+ PROJECT_FILES_MOVE_SELECTION_DOWN,
+ PROJECT_FILES_OPEN_SELECTION,
+ PROJECT_FILES_GO_BACK,
+ PROJECT_FILES_GO_TO_PERMALINK,
+ ],
+};
+
+export const ISSUABLE_SHORTCUTS_GROUP = {
+ id: 'issuables',
+ name: __('Epics, Issues, and Merge Requests'),
+ keybindings: [ISSUABLE_COMMENT_OR_REPLY, ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_CHANGE_LABEL],
+};
+
+export const ISSUE_MR_SHORTCUTS_GROUP = {
+ id: 'issuesMRs',
+ name: __('Issues and Merge Requests'),
+ keybindings: [ISSUE_MR_CHANGE_ASSIGNEE, ISSUE_MR_CHANGE_MILESTONE],
};
-export const WEB_IDE_GROUP = {
+export const MR_SHORTCUTS_GROUP = {
+ id: 'mergeRequests',
+ name: __('Merge Requests'),
+ keybindings: [
+ MR_NEXT_FILE_IN_DIFF,
+ MR_PREVIOUS_FILE_IN_DIFF,
+ MR_GO_TO_FILE,
+ MR_NEXT_UNRESOLVED_DISCUSSION,
+ MR_PREVIOUS_UNRESOLVED_DISCUSSION,
+ MR_COPY_SOURCE_BRANCH_NAME,
+ ],
+};
+
+export const MR_COMMITS_SHORTCUTS_GROUP = {
+ id: 'mergeRequestCommits',
+ name: __('Merge Request Commits'),
+ keybindings: [MR_COMMITS_NEXT_COMMIT, MR_COMMITS_PREVIOUS_COMMIT],
+};
+
+export const ISSUES_SHORTCUTS_GROUP = {
+ id: 'issues',
+ name: __('Issues'),
+ keybindings: [ISSUE_NEXT_DESIGN, ISSUE_PREVIOUS_DESIGN, ISSUE_CLOSE_DESIGN],
+};
+
+export const WEB_IDE_SHORTCUTS_GROUP = {
id: 'webIDE',
- name: s__('KeyboardShortcuts|Web IDE'),
- keybindings: [WEB_IDE_COMMIT],
+ name: __('Web IDE'),
+ keybindings: [WEB_IDE_GO_TO_FILE, WEB_IDE_COMMIT],
+};
+
+export const METRICS_SHORTCUTS_GROUP = {
+ id: 'metrics',
+ name: __('Metrics'),
+ keybindings: [
+ METRICS_EXPAND_PANEL,
+ METRICS_VIEW_LOGS,
+ METRICS_DOWNLOAD_CSV,
+ METRICS_COPY_LINK_TO_CHART,
+ METRICS_SHOW_ALERTS,
+ ],
+};
+
+export const MISC_SHORTCUTS_GROUP = {
+ id: 'misc',
+ name: __('Miscellaneous'),
+ keybindings: [TOGGLE_CANARY],
};
/** All keybindings, grouped and ordered with descriptions */
-export const keybindingGroups = [GLOBAL_SHORTCUTS_GROUP, WEB_IDE_GROUP];
+export const keybindingGroups = [
+ GLOBAL_SHORTCUTS_GROUP,
+ EDITING_SHORTCUTS_GROUP,
+ WIKI_SHORTCUTS_GROUP,
+ REPOSITORY_GRAPH_SHORTCUTS_GROUP,
+ PROJECT_SHORTCUTS_GROUP,
+ PROJECT_FILES_SHORTCUTS_GROUP,
+ ISSUABLE_SHORTCUTS_GROUP,
+ ISSUE_MR_SHORTCUTS_GROUP,
+ MR_SHORTCUTS_GROUP,
+ MR_COMMITS_SHORTCUTS_GROUP,
+ ISSUES_SHORTCUTS_GROUP,
+ WEB_IDE_SHORTCUTS_GROUP,
+ METRICS_SHORTCUTS_GROUP,
+ MISC_SHORTCUTS_GROUP,
+];
/**
* Gets keyboard shortcuts associated with a command
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index e4ec68601e0..03cba78cf31 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -6,13 +6,29 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import findAndFollowLink from '~/lib/utils/navigation_utility';
import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
-
-import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
+import {
+ keysFor,
+ TOGGLE_KEYBOARD_SHORTCUTS_DIALOG,
+ START_SEARCH,
+ FOCUS_FILTER_BAR,
+ TOGGLE_PERFORMANCE_BAR,
+ TOGGLE_CANARY,
+ TOGGLE_MARKDOWN_PREVIEW,
+ GO_TO_YOUR_TODO_LIST,
+ GO_TO_ACTIVITY_FEED,
+ GO_TO_YOUR_ISSUES,
+ GO_TO_YOUR_MERGE_REQUESTS,
+ GO_TO_YOUR_PROJECTS,
+ GO_TO_YOUR_GROUPS,
+ GO_TO_MILESTONE_LIST,
+ GO_TO_YOUR_SNIPPETS,
+ GO_TO_PROJECT_FIND_FILE,
+} from './keybindings';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
- if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
+ if (keysFor(TOGGLE_MARKDOWN_PREVIEW).indexOf(combo) !== -1) {
return false;
}
@@ -58,28 +74,41 @@ export default class Shortcuts {
this.helpModalElement = null;
this.helpModalVueInstance = null;
- Mousetrap.bind('?', this.onToggleHelp);
- Mousetrap.bind('s', Shortcuts.focusSearch);
- Mousetrap.bind('/', Shortcuts.focusSearch);
- Mousetrap.bind('f', this.focusFilter.bind(this));
+ Mousetrap.bind(keysFor(TOGGLE_KEYBOARD_SHORTCUTS_DIALOG), this.onToggleHelp);
+ Mousetrap.bind(keysFor(START_SEARCH), Shortcuts.focusSearch);
+ Mousetrap.bind(keysFor(FOCUS_FILTER_BAR), this.focusFilter.bind(this));
Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar);
Mousetrap.bind(keysFor(TOGGLE_CANARY), Shortcuts.onToggleCanary);
const findFileURL = document.body.dataset.findFile;
- Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
- Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
- Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
- Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
- Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
- Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
- Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
- Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
-
- Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview);
+ Mousetrap.bind(keysFor(GO_TO_YOUR_TODO_LIST), () => findAndFollowLink('.shortcuts-todos'));
+ Mousetrap.bind(keysFor(GO_TO_ACTIVITY_FEED), () =>
+ findAndFollowLink('.dashboard-shortcuts-activity'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_YOUR_ISSUES), () =>
+ findAndFollowLink('.dashboard-shortcuts-issues'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_YOUR_MERGE_REQUESTS), () =>
+ findAndFollowLink('.dashboard-shortcuts-merge_requests'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_YOUR_PROJECTS), () =>
+ findAndFollowLink('.dashboard-shortcuts-projects'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_YOUR_GROUPS), () =>
+ findAndFollowLink('.dashboard-shortcuts-groups'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_MILESTONE_LIST), () =>
+ findAndFollowLink('.dashboard-shortcuts-milestones'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_YOUR_SNIPPETS), () =>
+ findAndFollowLink('.dashboard-shortcuts-snippets'),
+ );
+
+ Mousetrap.bind(keysFor(TOGGLE_MARKDOWN_PREVIEW), Shortcuts.toggleMarkdownPreview);
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
- Mousetrap.bind('t', () => {
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_FIND_FILE), () => {
visitUrl(findFileURL);
});
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
index 11b4fcd4e1c..ab7fcbb35f1 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
@@ -1,4 +1,5 @@
import Mousetrap from 'mousetrap';
+import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
import {
getLocationHash,
updateHistory,
@@ -28,7 +29,7 @@ export default class ShortcutsBlob extends Shortcuts {
this.shortcircuitPermalinkButton();
- Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
+ Mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.moveToFilePermalink.bind(this));
}
moveToFilePermalink() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
index f0d2ecfd210..992e571e596 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
@@ -1,4 +1,11 @@
import Mousetrap from 'mousetrap';
+import {
+ keysFor,
+ PROJECT_FILES_MOVE_SELECTION_UP,
+ PROJECT_FILES_MOVE_SELECTION_DOWN,
+ PROJECT_FILES_OPEN_SELECTION,
+ PROJECT_FILES_GO_BACK,
+} from '~/behaviors/shortcuts/keybindings';
import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsFindFile extends ShortcutsNavigation {
@@ -10,7 +17,10 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) {
if (
element === projectFindFile.inputElement[0] &&
- (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
+ (keysFor(PROJECT_FILES_MOVE_SELECTION_UP).includes(combo) ||
+ keysFor(PROJECT_FILES_MOVE_SELECTION_DOWN).includes(combo) ||
+ keysFor(PROJECT_FILES_GO_BACK).includes(combo) ||
+ keysFor(PROJECT_FILES_OPEN_SELECTION).includes(combo))
) {
// when press up/down key in textbox, cursor prevent to move to home/end
e.preventDefault();
@@ -20,9 +30,9 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
return oldStopCallback.call(this, e, element, combo);
};
- Mousetrap.bind('up', projectFindFile.selectRowUp);
- Mousetrap.bind('down', projectFindFile.selectRowDown);
- Mousetrap.bind('esc', projectFindFile.goToTree);
- Mousetrap.bind('enter', projectFindFile.goToBlob);
+ Mousetrap.bind(keysFor(PROJECT_FILES_MOVE_SELECTION_UP), projectFindFile.selectRowUp);
+ Mousetrap.bind(keysFor(PROJECT_FILES_MOVE_SELECTION_DOWN), projectFindFile.selectRowDown);
+ Mousetrap.bind(keysFor(PROJECT_FILES_GO_BACK), projectFindFile.goToTree);
+ Mousetrap.bind(keysFor(PROJECT_FILES_OPEN_SELECTION), projectFindFile.goToBlob);
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 476745beb19..a55bdf231c0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -5,18 +5,33 @@ import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
import Sidebar from '../../right_sidebar';
import { CopyAsGFM } from '../markdown/copy_as_gfm';
+import {
+ keysFor,
+ ISSUE_MR_CHANGE_ASSIGNEE,
+ ISSUE_MR_CHANGE_MILESTONE,
+ ISSUABLE_CHANGE_LABEL,
+ ISSUABLE_COMMENT_OR_REPLY,
+ ISSUABLE_EDIT_DESCRIPTION,
+ MR_COPY_SOURCE_BRANCH_NAME,
+} from './keybindings';
import Shortcuts from './shortcuts';
export default class ShortcutsIssuable extends Shortcuts {
constructor() {
super();
- Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
- Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
- Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
- Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
- Mousetrap.bind('e', ShortcutsIssuable.editIssue);
- Mousetrap.bind('b', ShortcutsIssuable.copyBranchName);
+ Mousetrap.bind(keysFor(ISSUE_MR_CHANGE_ASSIGNEE), () =>
+ ShortcutsIssuable.openSidebarDropdown('assignee'),
+ );
+ Mousetrap.bind(keysFor(ISSUE_MR_CHANGE_MILESTONE), () =>
+ ShortcutsIssuable.openSidebarDropdown('milestone'),
+ );
+ Mousetrap.bind(keysFor(ISSUABLE_CHANGE_LABEL), () =>
+ ShortcutsIssuable.openSidebarDropdown('labels'),
+ );
+ Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), ShortcutsIssuable.replyWithSelectedText);
+ Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue);
+ Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), ShortcutsIssuable.copyBranchName);
}
static replyWithSelectedText() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index b46b4132ba8..b188d3b0ec3 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -1,27 +1,63 @@
import Mousetrap from 'mousetrap';
import findAndFollowLink from '../../lib/utils/navigation_utility';
+import {
+ keysFor,
+ GO_TO_PROJECT_OVERVIEW,
+ GO_TO_PROJECT_ACTIVITY_FEED,
+ GO_TO_PROJECT_RELEASES,
+ GO_TO_PROJECT_FILES,
+ GO_TO_PROJECT_COMMITS,
+ GO_TO_PROJECT_JOBS,
+ GO_TO_PROJECT_REPO_GRAPH,
+ GO_TO_PROJECT_REPO_CHARTS,
+ GO_TO_PROJECT_ISSUES,
+ GO_TO_PROJECT_ISSUE_BOARDS,
+ GO_TO_PROJECT_MERGE_REQUESTS,
+ GO_TO_PROJECT_WIKI,
+ GO_TO_PROJECT_SNIPPETS,
+ GO_TO_PROJECT_KUBERNETES,
+ GO_TO_PROJECT_ENVIRONMENTS,
+ GO_TO_PROJECT_METRICS,
+ NEW_ISSUE,
+} from './keybindings';
import Shortcuts from './shortcuts';
export default class ShortcutsNavigation extends Shortcuts {
constructor() {
super();
- Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
- Mousetrap.bind('g r', () => findAndFollowLink('.shortcuts-project-releases'));
- Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
- Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
- Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
- Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
- Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
- Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
- Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
- Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
- Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
- Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
- Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
- Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_OVERVIEW), () => findAndFollowLink('.shortcuts-project'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_ACTIVITY_FEED), () =>
+ findAndFollowLink('.shortcuts-project-activity'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_RELEASES), () =>
+ findAndFollowLink('.shortcuts-project-releases'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_FILES), () => findAndFollowLink('.shortcuts-tree'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_COMMITS), () => findAndFollowLink('.shortcuts-commits'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_JOBS), () => findAndFollowLink('.shortcuts-builds'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_REPO_GRAPH), () =>
+ findAndFollowLink('.shortcuts-network'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_REPO_CHARTS), () =>
+ findAndFollowLink('.shortcuts-repository-charts'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_ISSUES), () => findAndFollowLink('.shortcuts-issues'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_ISSUE_BOARDS), () =>
+ findAndFollowLink('.shortcuts-issue-boards'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_MERGE_REQUESTS), () =>
+ findAndFollowLink('.shortcuts-merge_requests'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_WIKI), () => findAndFollowLink('.shortcuts-wiki'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_SNIPPETS), () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_KUBERNETES), () =>
+ findAndFollowLink('.shortcuts-kubernetes'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_ENVIRONMENTS), () =>
+ findAndFollowLink('.shortcuts-environments'),
+ );
+ Mousetrap.bind(keysFor(GO_TO_PROJECT_METRICS), () => findAndFollowLink('.shortcuts-metrics'));
+ Mousetrap.bind(keysFor(NEW_ISSUE), () => findAndFollowLink('.shortcuts-new-issue'));
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
index 3e791e4673a..c33c092b009 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
@@ -1,15 +1,24 @@
import Mousetrap from 'mousetrap';
+import {
+ keysFor,
+ REPO_GRAPH_SCROLL_BOTTOM,
+ REPO_GRAPH_SCROLL_DOWN,
+ REPO_GRAPH_SCROLL_LEFT,
+ REPO_GRAPH_SCROLL_RIGHT,
+ REPO_GRAPH_SCROLL_TOP,
+ REPO_GRAPH_SCROLL_UP,
+} from './keybindings';
import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsNetwork extends ShortcutsNavigation {
constructor(graph) {
super();
- Mousetrap.bind(['left', 'h'], graph.scrollLeft);
- Mousetrap.bind(['right', 'l'], graph.scrollRight);
- Mousetrap.bind(['up', 'k'], graph.scrollUp);
- Mousetrap.bind(['down', 'j'], graph.scrollDown);
- Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
- Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_LEFT), graph.scrollLeft);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_RIGHT), graph.scrollRight);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_UP), graph.scrollUp);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_DOWN), graph.scrollDown);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_TOP), graph.scrollTop);
+ Mousetrap.bind(keysFor(REPO_GRAPH_SCROLL_BOTTOM), graph.scrollBottom);
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index c609936a02a..59c1d2654bc 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -1,11 +1,12 @@
import Mousetrap from 'mousetrap';
import findAndFollowLink from '../../lib/utils/navigation_utility';
+import { keysFor, EDIT_WIKI_PAGE } from './keybindings';
import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
- Mousetrap.bind('e', ShortcutsWiki.editWiki);
+ Mousetrap.bind(keysFor(EDIT_WIKI_PAGE), ShortcutsWiki.editWiki);
}
static editWiki() {
diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
index 6091a3183ac..8535f818b9c 100644
--- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
@@ -2,6 +2,11 @@
/* global Mousetrap */
import 'mousetrap';
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import {
+ keysFor,
+ ISSUE_PREVIOUS_DESIGN,
+ ISSUE_NEXT_DESIGN,
+} from '~/behaviors/shortcuts/keybindings';
import { s__, sprintf } from '~/locale';
import allDesignsMixin from '../../mixins/all_designs';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@@ -46,11 +51,14 @@ export default {
},
},
mounted() {
- Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign));
- Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign));
+ Mousetrap.bind(keysFor(ISSUE_PREVIOUS_DESIGN), () =>
+ this.navigateToDesign(this.previousDesign),
+ );
+ Mousetrap.bind(keysFor(ISSUE_NEXT_DESIGN), () => this.navigateToDesign(this.nextDesign));
},
beforeDestroy() {
- Mousetrap.unbind(['left', 'right'], this.navigateToDesign);
+ Mousetrap.unbind(keysFor(ISSUE_PREVIOUS_DESIGN));
+ Mousetrap.unbind(keysFor(ISSUE_NEXT_DESIGN));
},
methods: {
navigateToDesign(design) {
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 8a11c25a795..ad78433c7ce 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -2,6 +2,7 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
+import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -171,7 +172,7 @@ export default {
},
},
mounted() {
- Mousetrap.bind('esc', this.closeDesign);
+ Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign);
this.trackPageViewEvent();
// Set active discussion immediately.
@@ -180,7 +181,7 @@ export default {
this.updateActiveDiscussionFromUrl();
},
beforeDestroy() {
- Mousetrap.unbind('esc', this.closeDesign);
+ Mousetrap.unbind(keysFor(ISSUE_CLOSE_DESIGN));
},
methods: {
addImageDiffNoteToStore(store, { data: { createImageDiffNote } }) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 253e1e3b70e..98f1ee9242f 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,13 @@ import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Mousetrap from 'mousetrap';
import { mapState, mapGetters, mapActions } from 'vuex';
+import {
+ keysFor,
+ MR_PREVIOUS_FILE_IN_DIFF,
+ MR_NEXT_FILE_IN_DIFF,
+ MR_COMMITS_NEXT_COMMIT,
+ MR_COMMITS_PREVIOUS_COMMIT,
+} from '~/behaviors/shortcuts/keybindings';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
@@ -406,30 +413,23 @@ export default {
}
},
setEventListeners() {
- Mousetrap.bind(['[', 'k', ']', 'j'], (e, combo) => {
- switch (combo) {
- case '[':
- case 'k':
- this.jumpToFile(-1);
- break;
- case ']':
- case 'j':
- this.jumpToFile(+1);
- break;
- default:
- break;
- }
- });
+ Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
+ Mousetrap.bind(keysFor(MR_NEXT_FILE_IN_DIFF), () => this.jumpToFile(+1));
if (this.commit) {
- Mousetrap.bind('c', () => this.moveToNeighboringCommit({ direction: 'next' }));
- Mousetrap.bind('x', () => this.moveToNeighboringCommit({ direction: 'previous' }));
+ Mousetrap.bind(keysFor(MR_COMMITS_NEXT_COMMIT), () =>
+ this.moveToNeighboringCommit({ direction: 'next' }),
+ );
+ Mousetrap.bind(keysFor(MR_COMMITS_PREVIOUS_COMMIT), () =>
+ this.moveToNeighboringCommit({ direction: 'previous' }),
+ );
}
},
removeEventListeners() {
- Mousetrap.unbind(['[', 'k', ']', 'j']);
- Mousetrap.unbind('c');
- Mousetrap.unbind('x');
+ Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
+ Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF));
+ Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT));
+ Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT));
},
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 22c648a76a7..4fed7f555f6 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -46,6 +46,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
+ currentClearStatusAfter,
} = setStatusModalWrapperEl.dataset;
return {
@@ -54,6 +55,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
+ currentClearStatusAfter,
};
},
render(createElement) {
@@ -63,6 +65,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
+ currentClearStatusAfter,
} = this;
return createElement(SetStatusModalWrapper, {
@@ -72,6 +75,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
+ currentClearStatusAfter,
},
});
},
diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue
index fa3c900c337..7e8bb75902b 100644
--- a/app/assets/javascripts/notes/components/discussion_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_navigator.vue
@@ -1,6 +1,11 @@
<script>
/* global Mousetrap */
import 'mousetrap';
+import {
+ keysFor,
+ MR_NEXT_UNRESOLVED_DISCUSSION,
+ MR_PREVIOUS_UNRESOLVED_DISCUSSION,
+} from '~/behaviors/shortcuts/keybindings';
import eventHub from '~/notes/event_hub';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
@@ -10,12 +15,12 @@ export default {
eventHub.$on('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
},
mounted() {
- Mousetrap.bind('n', this.jumpToNextDiscussion);
- Mousetrap.bind('p', this.jumpToPreviousDiscussion);
+ Mousetrap.bind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION), this.jumpToNextDiscussion);
+ Mousetrap.bind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION), this.jumpToPreviousDiscussion);
},
beforeDestroy() {
- Mousetrap.unbind('n');
- Mousetrap.unbind('p');
+ Mousetrap.unbind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION));
+ Mousetrap.unbind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION));
eventHub.$off('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
},
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index bed264341a5..bff90254c04 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,14 +1,23 @@
<script>
/* eslint-disable vue/no-v-html */
-import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
+import {
+ GlToast,
+ GlModal,
+ GlTooltipDirective,
+ GlIcon,
+ GlFormCheckbox,
+ GlDropdown,
+ GlDropdownItem,
+} from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { updateUserStatus } from '~/rest_api';
+import { timeRanges } from '~/vue_shared/constants';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy } from './utils';
@@ -20,11 +29,21 @@ export const AVAILABILITY_STATUS = {
Vue.use(GlToast);
+const statusTimeRanges = [
+ {
+ label: __('Never'),
+ name: 'never',
+ },
+ ...timeRanges,
+];
+
export default {
components: {
GlIcon,
GlModal,
GlFormCheckbox,
+ GlDropdown,
+ GlDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -53,6 +72,11 @@ export default {
required: false,
default: false,
},
+ currentClearStatusAfter: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -65,6 +89,10 @@ export default {
modalId: 'set-user-status-modal',
noEmoji: true,
availability: isUserBusy(this.currentAvailability),
+ clearStatusAfter: statusTimeRanges[0].label,
+ clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), {
+ date: this.currentClearStatusAfter,
+ }),
};
},
computed: {
@@ -161,12 +189,16 @@ export default {
this.setStatus();
},
setStatus() {
- const { emoji, message, availability } = this;
+ const { emoji, message, availability, clearStatusAfter } = this;
updateUserStatus({
emoji,
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
+ clearStatusAfter:
+ clearStatusAfter === statusTimeRanges[0].label
+ ? null
+ : clearStatusAfter.replace(' ', '_'),
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
@@ -183,7 +215,11 @@ export default {
this.closeModal();
},
+ setClearStatusAfter(after) {
+ this.clearStatusAfter = after;
+ },
},
+ statusTimeRanges,
};
</script>
@@ -268,10 +304,31 @@ export default {
</div>
<div class="gl-display-flex">
<span class="gl-text-gray-600 gl-ml-5">
- {{ s__('SetStatusModal|"Busy" will be shown next to your name') }}
+ {{ s__('SetStatusModal|A busy indicator is shown next to your name and avatar.') }}
</span>
</div>
</div>
+ <div class="form-group">
+ <div class="gl-display-flex gl-align-items-baseline">
+ <span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span>
+ <gl-dropdown :text="clearStatusAfter" data-testid="clear-status-at-dropdown">
+ <gl-dropdown-item
+ v-for="after in $options.statusTimeRanges"
+ :key="after.name"
+ :data-testid="after.name"
+ @click="setClearStatusAfter(after.label)"
+ >{{ after.label }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
+ <div
+ v-if="currentClearStatusAfter.length"
+ class="gl-mt-3 gl-text-gray-400 gl-font-sm"
+ data-testid="clear-status-at-message"
+ >
+ {{ clearStatusAfterMessage }}
+ </div>
+ </div>
</div>
</div>
</gl-modal>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index 4ec54b33bce..fbadb202d51 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -3,6 +3,7 @@ import { GlIcon } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list';
+import { keysFor, MR_GO_TO_FILE } from '~/behaviors/shortcuts/keybindings';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import Item from './item.vue';
@@ -128,7 +129,7 @@ export default {
this.focusedIndex = 0;
}
- Mousetrap.bind(['t', 'mod+p'], (e) => {
+ Mousetrap.bind(keysFor(MR_GO_TO_FILE), (e) => {
if (e.preventDefault) {
e.preventDefault();
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 5bc1786d692..f52dc43aaff 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,7 @@
<script>
import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import $ from 'jquery';
+import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
@@ -116,6 +117,11 @@ export default {
.catch(() => {});
},
},
+ shortcuts: {
+ bold: keysFor(BOLD_TEXT),
+ italic: keysFor(ITALIC_TEXT),
+ link: keysFor(LINK_TEXT),
+ },
};
</script>
@@ -143,7 +149,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
- shortcuts="mod+b"
+ :shortcuts="$options.shortcuts.bold"
icon="bold"
/>
<toolbar-button
@@ -151,7 +157,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
- shortcuts="mod+i"
+ :shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
@@ -208,7 +214,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
- shortcuts="mod+k"
+ :shortcuts="$options.shortcuts.link"
icon="link"
/>
</div>
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index f24750243fc..82005c548f2 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -35,7 +35,6 @@ class GraphqlController < ApplicationController
def execute
result = multiplex? ? execute_multiplex : execute_query
-
render json: result
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 7ab5dc36e4a..eb083950fff 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -12,7 +12,6 @@ class GitlabSchema < GraphQL::Schema
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
- use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Pagination::Connections
use Gitlab::Graphql::GenericTracing
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index ac5ddc5bd4c..ec0f8b54789 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -2,7 +2,7 @@
module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
- prepend Gitlab::Graphql::Authorize::AuthorizeResource
+ include Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription
prepend ::Gitlab::Graphql::GlobalIDCompatibility
@@ -29,10 +29,30 @@ module Mutations
def ready?(**args)
if Gitlab::Database.read_only?
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE
+ raise_resource_not_available_error! ERROR_MESSAGE
else
true
end
end
+
+ def load_application_object(argument, lookup_as_type, id, context)
+ ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e|
+ Gitlab::ErrorTracking.track_exception(e)
+ # The default behaviour is to abort processing and return nil for the
+ # entire mutation field, but not set any top-level errors. We prefer to
+ # at least say that something went wrong.
+ raise_resource_not_available_error!
+ end
+ end
+
+ def self.authorized?(object, context)
+ # we never provide an object to mutations, but we do need to have a user.
+ context[:current_user].present? && !context[:current_user].blocked?
+ end
+
+ # See: AuthorizeResource#authorized_resource?
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
end
end
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index ce2126e26c0..f32205643da 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -52,13 +52,10 @@ module Mutations
super
end
- def resolve(board:, **args)
+ def resolve(board:, project_path:, iid:, **args)
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/247861')
- raise_resource_not_available_error! unless board
- authorize_board!(board)
-
- issue = authorized_find!(project_path: args[:project_path], iid: args[:iid])
+ issue = authorized_find!(project_path: project_path, iid: iid)
move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
move_issue(board, issue, move_params)
@@ -84,12 +81,6 @@ module Mutations
def move_arguments(args)
args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
end
-
- def authorize_board!(board)
- return if Ability.allowed?(current_user, :read_issue_board, board.resource_parent)
-
- raise_resource_not_available_error!
- end
end
end
end
diff --git a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
index 94a72bca7c7..fb6682f8d7e 100644
--- a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
+++ b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
module AlertManagement
class HttpIntegrationsResolver < BaseResolver
- alias_method :project, :synchronized_object
+ alias_method :project, :object
type Types::AlertManagement::HttpIntegrationType.connection_type, null: true
diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb
index 4d1fe367277..e027e0412bd 100644
--- a/app/graphql/resolvers/alert_management/integrations_resolver.rb
+++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
module AlertManagement
class IntegrationsResolver < BaseResolver
- alias_method :project, :synchronized_object
+ alias_method :project, :object
type Types::AlertManagement::IntegrationType.connection_type, null: true
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 67bba079512..7c408382cbe 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -138,16 +138,6 @@ module Resolvers
end
end
- # TODO: remove! This should never be necessary
- # Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/13984,
- # since once we use that authorization approach, the object is guaranteed to
- # be synchronized before any field.
- def synchronized_object
- strong_memoize(:synchronized_object) do
- ::Gitlab::Graphql::Lazy.force(object)
- end
- end
-
def single?
false
end
@@ -160,5 +150,13 @@ module Resolvers
def select_result(results)
results
end
+
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(try(:required_permissions))
+ end
+
+ def self.authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
end
end
diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb
index e66f7b97b40..0b699006626 100644
--- a/app/graphql/resolvers/board_lists_resolver.rb
+++ b/app/graphql/resolvers/board_lists_resolver.rb
@@ -3,13 +3,12 @@
module Resolvers
class BoardListsResolver < BaseResolver
include BoardIssueFilterable
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
type Types::BoardListType, null: true
- extras [:lookahead]
-
authorize :read_issue_board_list
+ authorizes_object!
argument :id, Types::GlobalIDType[List],
required: false,
@@ -21,15 +20,11 @@ module Resolvers
alias_method :board, :object
- def resolve(lookahead: nil, id: nil, issue_filters: {})
- authorize!(board)
-
+ def resolve_with_lookahead(id: nil, issue_filters: {})
lists = board_lists(id)
context.scoped_set!(:issue_filters, issue_filters(issue_filters))
- if load_preferences?(lookahead)
- List.preload_preferences_for_user(lists, current_user)
- end
+ List.preload_preferences_for_user(lists, current_user) if load_preferences?
offset_pagination(lists)
end
@@ -46,9 +41,8 @@ module Resolvers
service.execute(board, create_default_lists: false)
end
- def load_preferences?(lookahead)
- lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) ||
- lookahead&.selection(:nodes)&.selects?(:collapsed)
+ def load_preferences?
+ node_selection&.selects?(:collapsed)
end
def extract_list_id(gid)
diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb
index 637d690e4cd..85362ab1422 100644
--- a/app/graphql/resolvers/board_resolver.rb
+++ b/app/graphql/resolvers/board_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
class BoardResolver < BaseResolver.single
- alias_method :parent, :synchronized_object
+ alias_method :parent, :object
type Types::BoardType, null: true
diff --git a/app/graphql/resolvers/concerns/manual_authorization.rb b/app/graphql/resolvers/concerns/manual_authorization.rb
deleted file mode 100644
index 182110b9594..00000000000
--- a/app/graphql/resolvers/concerns/manual_authorization.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: remove this entirely when framework authorization is released
-# See: https://gitlab.com/gitlab-org/gitlab/-/issues/290216
-module ManualAuthorization
- def resolve(**args)
- super
- rescue ::Gitlab::Graphql::Errors::ResourceNotAvailable
- nil
- end
-end
diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb
index 2bad974daf7..34a4c67bc56 100644
--- a/app/graphql/resolvers/group_merge_requests_resolver.rb
+++ b/app/graphql/resolvers/group_merge_requests_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class GroupMergeRequestsResolver < MergeRequestsResolver
include GroupIssuableResolver
- alias_method :group, :synchronized_object
+ alias_method :group, :object
type Types::MergeRequestType.connection_type, null: true
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
index 8fd33c6626e..1f7a4b48aae 100644
--- a/app/graphql/resolvers/merge_request_resolver.rb
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class MergeRequestResolver < BaseResolver.single
include ResolvesMergeRequests
- alias_method :project, :synchronized_object
+ alias_method :project, :object
type ::Types::MergeRequestType, null: true
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index ecbdaaa3f55..5994dc449f6 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -6,7 +6,7 @@ module Resolvers
type ::Types::MergeRequestType.connection_type, null: true
- alias_method :project, :synchronized_object
+ alias_method :project, :object
def self.accept_assignee
argument :assignee_username, GraphQL::STRING_TYPE,
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
index 944b61f0c3a..c94e3d9e1d8 100644
--- a/app/graphql/resolvers/milestones_resolver.rb
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -56,7 +56,7 @@ module Resolvers
end
def parent
- synchronized_object
+ object
end
def parent_id_parameters(args)
diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb
index f618bf2df77..ec31a7dbe6d 100644
--- a/app/graphql/resolvers/projects/services_resolver.rb
+++ b/app/graphql/resolvers/projects/services_resolver.rb
@@ -3,11 +3,11 @@
module Resolvers
module Projects
class ServicesResolver < BaseResolver
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Projects::ServiceType.connection_type, null: true
authorize :admin_project
+ authorizes_object!
argument :active,
GraphQL::BOOLEAN_TYPE,
@@ -20,15 +20,7 @@ module Resolvers
alias_method :project, :object
- def resolve(**args)
- authorize!(project)
-
- services(args[:active], args[:type])
- end
-
- private
-
- def services(active, type)
+ def resolve(active: nil, type: nil)
servs = project.services
servs = servs.by_active_flag(active) unless active.nil?
servs = servs.by_type(type) unless type.blank?
diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb
index 569b82149d3..4328d38d485 100644
--- a/app/graphql/resolvers/snippets/blobs_resolver.rb
+++ b/app/graphql/resolvers/snippets/blobs_resolver.rb
@@ -3,12 +3,12 @@
module Resolvers
module Snippets
class BlobsResolver < BaseResolver
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Snippets::BlobType.connection_type, null: true
authorize :read_snippet
calls_gitaly!
+ authorizes_object!
alias_method :snippet, :object
@@ -17,7 +17,6 @@ module Resolvers
description: 'Paths of the blobs.'
def resolve(paths: [])
- authorize!(snippet)
return [snippet.blob] if snippet.empty_repo?
if paths.empty?
diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
index 47967fe69f9..0b39d3945f6 100644
--- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb
+++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
@@ -13,7 +13,7 @@ module Resolvers
description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.'
attr_reader :project
- alias_method :user, :synchronized_object
+ alias_method :user, :object
def ready?(project_id: nil, project_path: nil, **args)
return early_return unless can_read_profile?
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index 4d470aceca4..527269846ff 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -36,6 +36,18 @@ module Types
def enum
@enum_values ||= {}.with_indifferent_access
end
+
+ def authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
+
+ def authorize(*abilities)
+ @abilities = abilities
+ end
+
+ def authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
end
end
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 78ab6890923..99f32894925 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -2,7 +2,6 @@
module Types
class BaseField < GraphQL::Schema::Field
- prepend Gitlab::Graphql::Authorize
include GitlabStyleDeprecations
argument_class ::Types::BaseArgument
@@ -13,6 +12,7 @@ module Types
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
@constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0
@requires_argument = !!kwargs.delete(:requires_argument)
+ @authorize = Array.wrap(kwargs.delete(:authorize))
kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
@@ -22,8 +22,8 @@ module Types
# We want to avoid the overhead of this in prod
extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env?
-
extension ::Gitlab::Graphql::Present::FieldExtension
+ extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension
end
def may_call_gitaly?
@@ -34,6 +34,19 @@ module Types
@requires_argument || arguments.values.any? { |argument| argument.type.non_null? }
end
+ # By default fields authorize against the current object, but that is not how our
+ # resolvers work - they use declarative permissions to authorize fields
+ # manually (so we make them opt in).
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/300922
+ # (separate out authorize into permissions on the object, and on the
+ # resolved values)
+ # We do not support argument authorization in our schema. If/when we do,
+ # we should call `super` here, to apply argument authorization checks.
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/324647
+ def authorized?(object, args, ctx)
+ field_authorized?(object, ctx) && resolver_authorized?(object, ctx)
+ end
+
def base_complexity
complexity = DEFAULT_COMPLEXITY
complexity += 1 if calls_gitaly?
@@ -58,6 +71,26 @@ module Types
attr_reader :feature_flag
+ def field_authorized?(object, ctx)
+ authorization.ok?(object, ctx[:current_user])
+ end
+
+ # Historically our resolvers have used declarative permission checks only
+ # for _what they resolved_, not the _object they resolved these things from_
+ # We preserve these semantics here, and only apply resolver authorization
+ # if the resolver has opted in.
+ def resolver_authorized?(object, ctx)
+ if @resolver_class && @resolver_class.try(:authorizes_object?)
+ @resolver_class.authorized?(object, ctx)
+ else
+ true
+ end
+ end
+
+ def authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(@authorize)
+ end
+
def feature_documentation_message(key, description)
"#{description} Available only when feature flag `#{key}` is enabled."
end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
index 4b1f3193136..c21c95876be 100644
--- a/app/graphql/types/base_interface.rb
+++ b/app/graphql/types/base_interface.rb
@@ -5,5 +5,11 @@ module Types
include GraphQL::Schema::Interface
field_class ::Types::BaseField
+
+ definition_methods do
+ def authorized?(object, context)
+ resolve_type(object, context).authorized?(object, context)
+ end
+ end
end
end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index 9c36c83d4a3..cd677e50d28 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -19,6 +19,14 @@ module Types
GitlabSchema.id_from_object(object)
end
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
+
+ def self.authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
+
def current_user
context[:current_user]
end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
index 30a5668c0bb..aeafbf85020 100644
--- a/app/graphql/types/base_union.rb
+++ b/app/graphql/types/base_union.rb
@@ -2,5 +2,8 @@
module Types
class BaseUnion < GraphQL::Schema::Union
+ def self.authorized?(object, context)
+ resolve_type(object, context).authorized?(object, context)
+ end
end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 8920133734c..6997c8cffda 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -159,13 +159,20 @@ module PageLayoutHelper
end
def user_status_properties(user)
- default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI }
+ default_properties = {
+ current_emoji: '',
+ current_message: '',
+ can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml),
+ default_emoji: UserStatus::DEFAULT_EMOJI
+ }
+
return default_properties unless user&.status
default_properties.merge({
current_emoji: user.status.emoji.to_s,
current_message: user.status.message.to_s,
- current_availability: user.status.availability.to_s
+ current_availability: user.status.availability.to_s,
+ current_clear_status_after: user.status.clear_status_at.to_s
})
end
diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb
index 9960fb4bf12..6640b0c5e94 100644
--- a/app/presenters/packages/detail/package_presenter.rb
+++ b/app/presenters/packages/detail/package_presenter.rb
@@ -64,7 +64,6 @@ module Packages
id: pipeline_info.id,
sha: pipeline_info.sha,
ref: pipeline_info.ref,
- git_commit_message: pipeline_info.git_commit_message,
user: build_user_info(pipeline_info.user),
project: {
name: pipeline_info.project.name,
diff --git a/changelogs/unreleased/262086-user-availability-allow-users-to-schedule-un-setting-of-their-stat.yml b/changelogs/unreleased/262086-user-availability-allow-users-to-schedule-un-setting-of-their-stat.yml
new file mode 100644
index 00000000000..13373e67c5a
--- /dev/null
+++ b/changelogs/unreleased/262086-user-availability-allow-users-to-schedule-un-setting-of-their-stat.yml
@@ -0,0 +1,5 @@
+---
+title: User Availability - Allow users to schedule un-setting of their status values
+merge_request: 56649
+author:
+type: added
diff --git a/changelogs/unreleased/56716-remove-commit-message-package-ui.yml b/changelogs/unreleased/56716-remove-commit-message-package-ui.yml
new file mode 100644
index 00000000000..af7380c1726
--- /dev/null
+++ b/changelogs/unreleased/56716-remove-commit-message-package-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the commit message from the package details UI
+merge_request: 56716
+author:
+type: changed
diff --git a/config/feature_flags/development/diff_line_syntax_highlighting.yml b/config/feature_flags/development/diff_line_syntax_highlighting.yml
new file mode 100644
index 00000000000..3244dad6c24
--- /dev/null
+++ b/config/feature_flags/development/diff_line_syntax_highlighting.yml
@@ -0,0 +1,8 @@
+---
+name: diff_line_syntax_highlighting
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56108
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324159
+milestone: '13.10'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb
index f1bc289f1f0..52c26e756a5 100644
--- a/config/initializers/graphql.rb
+++ b/config/initializers/graphql.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
GraphQL::ObjectType.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
-GraphQL::Field.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
GraphQL::Schema::Object.accepts_definition(:authorize)
-GraphQL::Schema::Field.accepts_definition(:authorize)
diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md
index f8360e331b6..9d9c01b870f 100644
--- a/doc/administration/auth/ldap/ldap-troubleshooting.md
+++ b/doc/administration/auth/ldap/ldap-troubleshooting.md
@@ -668,7 +668,7 @@ adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -u
You can also retrieve a single object by **specifying** the object name or full **DN**. In this example we specify the object name only `CN=Leroy Fox`.
```shell
-adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f (&(objectcategory=person)(CN=Leroy Fox))”
+adfind -h ad.example.org:636 -ssl -u "CN=GitLabSRV,CN=Users,DC=GitLab,DC=org" -up Password1 -b "OU=GitLab INT,DC=GitLab,DC=org" -f "(&(objectcategory=person)(CN=Leroy Fox))"
```
### Rails console
diff --git a/doc/administration/geo/replication/security_review.md b/doc/administration/geo/replication/security_review.md
index f4f6cb50184..abb84b95623 100644
--- a/doc/administration/geo/replication/security_review.md
+++ b/doc/administration/geo/replication/security_review.md
@@ -34,7 +34,7 @@ from [owasp.org](https://owasp.org/).
### How can the data be classified into categories according to its sensitivity?
- The GitLab model of sensitivity is centered around public vs. internal vs.
- private projects. Geo replicates them all indiscriminately. “Selective sync”
+ private projects. Geo replicates them all indiscriminately. "Selective sync"
exists for files and repositories (but not database content), which would permit
only less-sensitive projects to be replicated to a **secondary** node if desired.
- See also: [GitLab data classification policy](https://about.gitlab.com/handbook/engineering/security/data-classification-standard.html).
@@ -165,7 +165,7 @@ from [owasp.org](https://owasp.org/).
### What aspects of the product may or may not be hosted via the cloud computing model?
-- GitLab is “cloud native” and this applies to Geo as much as to the rest of the
+- GitLab is "cloud native" and this applies to Geo as much as to the rest of the
product. Deployment in clouds is a common and supported scenario.
## If applicable, what approach(es) to cloud computing will be taken (Managed Hosting versus "Pure" Cloud, a "full machine" approach such as AWS-EC2 versus a "hosted database" approach such as AWS-RDS and Azure, etc)?
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 84838a5052e..25ef2f9a850 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -8,18 +8,9 @@ type: reference
# Gitaly Cluster **(FREE SELF)**
[Gitaly](index.md), the service that provides storage for Git repositories, can
-be run in a clustered configuration to increase fault tolerance. In this
-configuration, every Git repository is stored on every Gitaly node in the
-cluster. Multiple clusters (or storage shards) can be configured.
-
-NOTE:
-Technical support for Gitaly clusters is limited to GitLab Premium and Ultimate
-customers.
-
-Praefect is a router and transaction manager for Gitaly, and a required
-component for running a Gitaly Cluster.
-
-![Architecture diagram](img/praefect_architecture_v12_10.png)
+be run in a clustered configuration to scale the Gitaly service and increase
+fault tolerance. In this configuration, every Git repository is stored on every
+Gitaly node in the cluster.
Using a Gitaly Cluster increases fault tolerance by:
@@ -27,6 +18,10 @@ Using a Gitaly Cluster increases fault tolerance by:
- Detecting Gitaly node failures.
- Automatically routing Git requests to an available Gitaly node.
+NOTE:
+Technical support for Gitaly clusters is limited to GitLab Premium and Ultimate
+customers.
+
The availability objectives for Gitaly clusters are:
- **Recovery Point Objective (RPO):** Less than 1 minute.
@@ -57,28 +52,48 @@ Follow the [Gitaly Cluster epic](https://gitlab.com/groups/gitlab-org/-/epics/14
for improvements including
[horizontally distributing reads](https://gitlab.com/groups/gitlab-org/-/epics/2013).
-## Gitaly Cluster compared to Geo
+## Overview
-Gitaly Cluster and [Geo](../geo/index.md) both provide redundancy. However the redundancy of:
+Git storage is provided through the Gitaly service in GitLab, and is essential
+to correct proper operation of the GitLab application. When the number of
+users, repositories, and activity grows, it is important to scale Gitaly
+appropriately by:
-- Gitaly Cluster provides fault tolerance for data storage and is invisible to the user. Users are
- not aware when Gitaly Cluster is used.
-- Geo provides [replication](../geo/index.md) and [disaster recovery](../geo/disaster_recovery/index.md) for
- an entire instance of GitLab. Users know when they are using Geo for
- [replication](../geo/index.md). Geo [replicates multiple data types](../geo/replication/datatypes.md#limitations-on-replicationverification),
- including Git data.
+- Increasing the available CPU and memory resources available to Git before
+ resource exhaustion degrades Git, Gitaly, and GitLab application performance.
+- Increase available storage before storage limits are reached causing write
+ operations to fail.
+- Improve fault tolerance by removing single points of failure. Git should be
+ considered mission critical if a service degradation would prevent you from
+ deploying changes to production.
-The following table outlines the major differences between Gitaly Cluster and Geo:
+### Moving beyond NFS
-| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for |
-|:---------------|:---------|:----------|:-------------------|:-----------------------------------------------------|:------------------------------|:------------------------|
-| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](#automatic-failover-and-leader-election) | [Strong](#strong-consistency) | Data storage in Git |
-| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance |
+WARNING:
+From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
+support for NFS for Git repositories is scheduled to be removed. Upgrade to
+Gitaly Cluster as soon as possible.
-For more information, see:
+[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System)
+is not well suited to Git workloads which are CPU and IOPS sensitive.
+Specifically:
-- [Gitaly architecture](index.md#architecture).
-- Geo [use cases](../geo/index.md#use-cases) and [architecture](../geo/index.md#architecture).
+- Git is sensitive to file system latency. Even simple operations require many
+ read operations. Operations that are fast on block storage can become an order of
+ magnitude slower. This significantly impacts GitLab application performance.
+- NFS performance optimizations that prevent the performance gap between
+ block storage and NFS being even wider are vulnerable to race conditions. We have observed
+ [data inconsistencies](https://gitlab.com/gitlab-org/gitaly/-/issues/2589)
+ in production environments caused by simultaneous writes to different NFS
+ clients. Data corruption is not an acceptable risk.
+
+Gitaly Cluster is purpose built to provide reliable, high performance, fault
+tolerant Git storage.
+
+Further reading:
+
+- Blog post: [The road to Gitaly v1.0 (aka, why GitLab doesn't require NFS for storing Git data anymore)](https://about.gitlab.com/blog/2018/09/12/the-road-to-gitaly-1-0/)
+- Blog post: [How we spent two weeks hunting an NFS bug in the Linux kernel](https://about.gitlab.com/blog/2018/11/14/how-we-spent-two-weeks-hunting-an-nfs-bug/)
## Where Gitaly Cluster fits
@@ -154,6 +169,38 @@ A hybrid approach can be used in these instances, where each shard is configured
cluster. [Variable replication factor](https://gitlab.com/groups/gitlab-org/-/epics/3372) is planned
to provide greater flexibility for extremely large GitLab instances.
+### Gitaly Cluster compared to Geo
+
+Gitaly Cluster and [Geo](../geo/index.md) both provide redundancy. However the redundancy of:
+
+- Gitaly Cluster provides fault tolerance for data storage and is invisible to the user. Users are
+ not aware when Gitaly Cluster is used.
+- Geo provides [replication](../geo/index.md) and [disaster recovery](../geo/disaster_recovery/index.md) for
+ an entire instance of GitLab. Users know when they are using Geo for
+ [replication](../geo/index.md). Geo [replicates multiple datatypes](../geo/replication/datatypes.md#limitations-on-replicationverification),
+ including Git data.
+
+The following table outlines the major differences between Gitaly Cluster and Geo:
+
+| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for |
+|:---------------|:---------|:----------|:-------------------|:-----------------------------------------------------|:------------------------------|:------------------------|
+| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](#automatic-failover-and-leader-election) | [Strong](#strong-consistency) | Data storage in Git |
+| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance |
+
+For more information, see:
+
+- [Gitaly architecture](index.md#architecture).
+- Geo [use cases](../geo/index.md#use-cases) and [architecture](../geo/index.md#architecture).
+
+## Architecture
+
+Praefect is a router and transaction manager for Gitaly, and a required
+component for running a Gitaly Cluster.
+
+![Architecture diagram](img/praefect_architecture_v12_10.png)
+
+For more information, see [Gitaly HA Design](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md)
+
## Requirements for configuring a Gitaly Cluster
The minimum recommended configuration for a Gitaly Cluster requires:
@@ -204,7 +251,7 @@ You need the IP/host address for each node.
If you are using a cloud provider, you can look up the addresses for each server through your cloud provider's management console.
-If you are using Google Cloud Platform, SoftLayer, or any other vendor that provides a virtual private cloud (VPC) you can use the private addresses for each cloud instance (corresponds to “internal address” for Google Cloud Platform) for `PRAEFECT_HOST`, `GITALY_HOST_*`, and `GITLAB_HOST`.
+If you are using Google Cloud Platform, SoftLayer, or any other vendor that provides a virtual private cloud (VPC) you can use the private addresses for each cloud instance (corresponds to "internal address" for Google Cloud Platform) for `PRAEFECT_HOST`, `GITALY_HOST_*`, and `GITLAB_HOST`.
#### Secrets
diff --git a/doc/administration/troubleshooting/postgresql.md b/doc/administration/troubleshooting/postgresql.md
index e1d95e5b7e4..1da52e461af 100644
--- a/doc/administration/troubleshooting/postgresql.md
+++ b/doc/administration/troubleshooting/postgresql.md
@@ -87,7 +87,7 @@ This section is for links to information elsewhere in the GitLab documentation.
```plaintext
ERROR: replication slots can only be used if max_replication_slots > 0
- FATAL: could not start WAL streaming: ERROR: replication slot “geo_secondary_my_domain_com” does not exist
+ FATAL: could not start WAL streaming: ERROR: replication slot "geo_secondary_my_domain_com" does not exist
Command exceeded allowed execution time
diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md
index 1b7e273f7a1..fe92b17a121 100644
--- a/doc/api/graphql/getting_started.md
+++ b/doc/api/graphql/getting_started.md
@@ -34,7 +34,7 @@ curl "https://gitlab.com/api/graphql" --header "Authorization: Bearer $GRAPHQL_T
### GraphiQL
-GraphiQL (pronounced “graphical”) allows you to run queries directly against the server endpoint
+GraphiQL (pronounced "graphical") allows you to run queries directly against the server endpoint
with syntax highlighting and autocomplete. It also allows you to explore the schema and types.
The examples below:
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index bfc332e35b1..f2cb9500b2c 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -581,6 +581,9 @@ via the GitLab UI:
1. On the next push, your CI/CD job uses a new cache.
+NOTE:
+Each time you clear the cache manually, the [internal cache name](#where-the-caches-are-stored) is updated. The name uses the format `cache-<index>`, and the index increments by one each time. The old cache is not deleted. You can manually delete these files from the runner storage.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 9736f8c1418..6b570326a47 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -324,8 +324,9 @@ now trigger a pipeline on the current project's default branch. The maximum
number of upstream pipeline subscriptions is 2 by default, for both the upstream and
downstream projects. This [application limit](../administration/instance_limits.md#number-of-cicd-subscriptions-to-a-project) can be changed on self-managed instances by a GitLab administrator.
-The upstream project needs to be [public](../public_access/public_access.md) for
-pipeline subscription to work.
+The upstream project needs to be [public](../public_access/public_access.md)
+and the user must have [developer permissions](../user/permissions.md#project-members-permissions)
+for the upstream project.
## Downstream private projects confidentiality concern
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 220a4a107aa..df1511476e5 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -197,7 +197,7 @@ Check this [page](vuex.md) for more details.
In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows:
> The data object for the Vue instance. Vue recursively converts its properties into getter/setters
-to make it “reactive”. The object must be plain: native objects such as browser API objects and
+to make it "reactive". The object must be plain: native objects such as browser API objects and
prototype properties are ignored. A rule of thumb is that data should just be data - it is not
recommended to observe objects with their own stateful behavior.
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index 7fb49521106..acbc787942d 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -109,6 +109,6 @@ To propose additions to the glossary please
<!-- vale gitlab.Spelling = NO -->
In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000036068906/)).
-So, to include both genders, write “Utilisateurs et utilisatrices” instead of “Utilisateur·rice·s”.
+So, to include both genders, write "Utilisateurs et utilisatrices" instead of "Utilisateur·rice·s".
When space is missing, the male gender should be used alone.
<!-- vale gitlab.Spelling = YES -->
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index c63ce9e2b07..c55227efa7b 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -497,18 +497,20 @@ request, be sure to start the `dont-interrupt-me` job before pushing.
1. We currently have several different caches defined in
[`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml),
with fixed keys:
- - `.setup-test-env-cache`.
- - `.rails-cache`.
- - `.static-analysis-cache`.
+ - `.setup-test-env-cache`
+ - `.rails-cache`
+ - `.static-analysis-cache`
- `.coverage-cache`
+ - `.danger-review-cache`
- `.qa-cache`
- - `.yarn-cache`.
+ - `.yarn-cache`
- `.assets-compile-cache` (the key includes `${NODE_ENV}` so it's actually two different caches).
1. Only 6 specific jobs, running in 2-hourly scheduled pipelines, are pushing (i.e. updating) to the caches:
- `update-setup-test-env-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-rails-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-static-analysis-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
- `update-coverage-cache`, defined in [`.gitlab/ci/rails.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/rails.gitlab-ci.yml).
+ - `update-danger-review-cache`, defined in [`.gitlab/ci/review.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/review.gitlab-ci.yml).
- `update-qa-cache`, defined in [`.gitlab/ci/qa.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/qa.gitlab-ci.yml).
- `update-assets-compile-production-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
- `update-assets-compile-test-cache`, defined in [`.gitlab/ci/frontend.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/frontend.gitlab-ci.yml).
@@ -614,6 +616,7 @@ that is deployed in stage `review`.
[`coverage-javascript`](https://gitlab-org.gitlab.io/gitlab/coverage-javascript/),
and `webpack-report` (found at `https://gitlab-org.gitlab.io/gitlab/webpack-report/`, but there is
[an issue with the deployment](https://gitlab.com/gitlab-org/gitlab/-/issues/233458)).
+- `notify`: This stage includes jobs that notify various failures to Slack.
### Default image
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 80835c4333d..47d6a39d625 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -9812,6 +9812,30 @@ Status: `implemented`
Tiers: `premium`, `ultimate`
+### `redis_hll_counters.epics_usage.g_project_management_users_destroying_epic_notes_monthly`
+
+Counts of MAU destroying epic notes
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315034808_g_project_management_users_destroying_epic_notes_monthly.yml)
+
+Group: `group:product planning`
+
+Status: `implemented`
+
+Tiers: `premium`, `ultimate`
+
+### `redis_hll_counters.epics_usage.g_project_management_users_destroying_epic_notes_weekly`
+
+Counts of WAU destroying epic notes
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315034846_g_project_management_users_destroying_epic_notes_weekly.yml)
+
+Group: `group:product planning`
+
+Status: `implemented`
+
+Tiers: `premium`, `ultimate`
+
### `redis_hll_counters.ide_edit.g_edit_by_sfe_monthly`
Missing description
diff --git a/doc/install/pivotal/index.md b/doc/install/pivotal/index.md
index eee70c2c578..ef6eb378346 100644
--- a/doc/install/pivotal/index.md
+++ b/doc/install/pivotal/index.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
As of September 13, 2017, the GitLab Enterprise Plus for Pivotal Cloud Foundry
-tile on Pivotal Network has reached its End of Availability (“EoA”) and is no
+tile on Pivotal Network has reached its End of Availability ("EoA") and is no
longer available for download or sale through Pivotal. Current customers with
active subscriptions continue to receive support from GitLab through their
subscription term. Pivotal and GitLab are collaborating on creating a new
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_10.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_10.png
new file mode 100644
index 00000000000..0490a8ed763
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_10.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_6.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_6.png
deleted file mode 100644
index 6ccae80e80e..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_chart_v13_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index e5942aea754..b8728a6ec29 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -71,17 +71,22 @@ CSV file containing details of the resources scanned.
## Project Security Dashboard
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in GitLab 13.6.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285476) in GitLab 13.10, options to zoom in on a date range, and download the vulnerabilities chart.
At the project level, the Security Dashboard displays a chart with the number of vulnerabilities over time.
Access it by navigating to **Security & Compliance > Security Dashboard**. We display historical
data up to 365 days. The chart's data is updated daily.
-![Project Security Dashboard](img/project_security_dashboard_chart_v13_6.png)
+![Project Security Dashboard](img/project_security_dashboard_chart_v13_10.png)
Filter the historical data by clicking on the corresponding legend name. The image above, for example, shows
only the graph for vulnerabilities with **high** severity.
+To zoom in, select the left-most icon, then select the desired rangeby dragging across the chart. Select **Remove Selection** (**{{redo}}**) to reset to the original date range.
+
+To download an SVG image of the chart, select **Save chart to an image** (**{download}**).
+
## Group Security Dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6709) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.5.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5bcfafa7196..77f7e66b9a5 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -106,7 +106,8 @@ To show private contributions:
## Set your current status
-> Introduced in GitLab 11.2.
+> - Introduced in GitLab 11.2.
+> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56649) in GitLab 13.10.
You can provide a custom status message for your user profile along with an emoji that describes it.
This may be helpful when you are out of office or otherwise not available.
@@ -119,6 +120,7 @@ To set your current status:
1. Select **Set status** or, if you have already set a status, **Edit status**.
1. Set the desired emoji and status message. Status messages must be plain text and 100 characters or less.
They can also contain emoji codes like, `I'm on vacation :palm_tree:`.
+1. Select a value from the **Clear status after** dropdown.
1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely.
You can also set your current status by [using the API](../../api/users.md#user-status).
diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md
index edef625b00b..e0be18d8b12 100644
--- a/doc/user/project/clusters/serverless/aws.md
+++ b/doc/user/project/clusters/serverless/aws.md
@@ -432,7 +432,7 @@ deploys your application. If your:
To test the application you deployed, please go to the build log and follow the following steps:
-1. Click on “Show complete raw” on the upper right-hand corner:
+1. Click on "Show complete raw" on the upper right-hand corner:
![sam-complete-raw](img/sam-complete-raw.png)
diff --git a/doc/user/project/integrations/jira_integrations.md b/doc/user/project/integrations/jira_integrations.md
index 205b417a833..6dcabdf8857 100644
--- a/doc/user/project/integrations/jira_integrations.md
+++ b/doc/user/project/integrations/jira_integrations.md
@@ -47,8 +47,8 @@ time.
| Capability | Jira integration | Jira Development Panel integration |
|:----------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------|
| Mention of Jira issue ID in GitLab is automatically linked to that issue | Yes | No |
-| Mention of Jira issue ID in GitLab issue/MR is reflected in the Jira issue | Yes, as a Jira comment with the GitLab issue/MR title and a link back to it. Its first mention also adds the GitLab page to the Jira issue under “Web links”. | Yes, in the issue's Development panel |
-| Mention of Jira issue ID in GitLab commit message is reflected in the issue | Yes. The entire commit message is added to the Jira issue as a comment and under “Web links”, each with a link back to the commit in GitLab. | Yes, in the issue's Development panel and optionally with a custom comment on the Jira issue using Jira Smart Commits. |
+| Mention of Jira issue ID in GitLab issue/MR is reflected in the Jira issue | Yes, as a Jira comment with the GitLab issue/MR title and a link back to it. Its first mention also adds the GitLab page to the Jira issue under "Web links". | Yes, in the issue's Development panel |
+| Mention of Jira issue ID in GitLab commit message is reflected in the issue | Yes. The entire commit message is added to the Jira issue as a comment and under "Web links", each with a link back to the commit in GitLab. | Yes, in the issue's Development panel and optionally with a custom comment on the Jira issue using Jira Smart Commits. |
| Mention of Jira issue ID in GitLab branch names is reflected in Jira issue | No | Yes, in the issue's Development panel |
| Record Jira time tracking information against an issue | No | Yes. Time can be specified via Jira Smart Commits. |
| Transition or close a Jira issue with a Git commit or merge request | Yes. Only a single transition type, typically configured to close the issue by setting it to Done. | Yes. Transition to any state using Jira Smart Commits. |
diff --git a/lefthook.yml b/lefthook.yml
index 29a7cfafd83..8c03a51a4c8 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -33,12 +33,8 @@ pre-push:
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.rb'
run: bundle exec rubocop --parallel --force-exclusion {files}
- vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters
+ vale: # Requires Vale: https://docs.gitlab.com/ee/development/documentation/#install-linters
tags: documentation style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: 'doc/*.md'
run: if command -v vale 2> /dev/null; then vale --config .vale.ini --minAlertLevel error {files}; else echo "Vale not found. Install Vale"; fi
- docs-metadata: # https://docs.gitlab.com/ee/development/documentation/#metadata
- tags: documentation style
- files: git diff --name-only $(git merge-base origin/master HEAD)..HEAD | grep '.md'
- run: if [[ $(head -n1 {files} | grep -v --count -- '---') -ge 1 ]]; then echo "$(tput setaf 1)Documentation metadata missing in changed Markdown files.$(tput sgr0) For more information, see https://docs.gitlab.com/ee/development/documentation/#metadata."; false; else echo "$(tput sgr0)Metadata found in {files}."; fi;
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 23859e2573e..8385bbbb3de 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -86,6 +86,41 @@ module Gitlab
def highlight_line(diff_line)
return unless diff_file && diff_file.diff_refs
+ if Feature.enabled?(:diff_line_syntax_highlighting, project, default_enabled: :yaml)
+ diff_line_highlighting(diff_line)
+ else
+ blob_highlighting(diff_line)
+ end
+ end
+
+ def diff_line_highlighting(diff_line)
+ rich_line = syntax_highlighter(diff_line).highlight(
+ diff_line.text(prefix: false),
+ context: { line_number: diff_line.line }
+ )&.html_safe
+
+ # Only update text if line is found. This will prevent
+ # issues with submodules given the line only exists in diff content.
+ if rich_line
+ line_prefix = diff_line.text =~ /\A(.)/ ? Regexp.last_match(1) : ' '
+ rich_line.prepend(line_prefix).concat("\n")
+ end
+ end
+
+ def syntax_highlighter(diff_line)
+ path = diff_line.removed? ? diff_file.old_path : diff_file.new_path
+
+ @syntax_highlighter ||= {}
+ @syntax_highlighter[path] ||= Gitlab::Highlight.new(
+ path,
+ @raw_lines,
+ language: repository&.gitattribute(path, 'gitlab-language')
+ )
+ end
+
+ # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/324159
+ # ------------------------------------------------------------------------
+ def blob_highlighting(diff_line)
rich_line =
if diff_line.unchanged? || diff_line.added?
new_lines[diff_line.new_pos - 1]&.html_safe
@@ -102,6 +137,7 @@ module Gitlab
end
# Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/324638
+ # ------------------------------------------------------------------------
def inline_diffs
@inline_diffs ||= InlineDiff.for_lines(@raw_lines)
end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 2192582348c..209462fd6e9 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -71,10 +71,12 @@ module Gitlab
strong_memoize(:redis_key) do
[
'highlighted-diff-files',
- diffable.cache_key, VERSION,
+ diffable.cache_key,
+ VERSION,
diff_options,
Feature.enabled?(:introduce_marker_ranges, diffable.project, default_enabled: :yaml),
- Feature.enabled?(:use_marker_ranges, diffable.project, default_enabled: :yaml)
+ Feature.enabled?(:use_marker_ranges, diffable.project, default_enabled: :yaml),
+ Feature.enabled?(:diff_line_syntax_highlighting, diffable.project, default_enabled: :yaml)
].join(":")
end
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 444928b4310..66f506ec3aa 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -9,8 +9,8 @@ module Gitlab
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
attr_reader :line_code, :marker_ranges
- attr_writer :rich_text
- attr_accessor :text, :index, :type, :old_pos, :new_pos
+ attr_writer :text, :rich_text
+ attr_accessor :index, :type, :old_pos, :new_pos
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
@text, @type, @index = text, type, index
@@ -54,6 +54,12 @@ module Gitlab
@marker_ranges = marker_ranges
end
+ def text(prefix: true)
+ return @text if prefix
+
+ @text&.slice(1..).to_s
+ end
+
def old_line
old_pos unless added? || meta?
end
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
deleted file mode 100644
index e83b567308b..00000000000
--- a/lib/gitlab/graphql/authorize.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- # Allow fields to declare permissions their objects must have. The field
- # will be set to nil unless all required permissions are present.
- module Authorize
- extend ActiveSupport::Concern
-
- def self.use(schema_definition)
- schema_definition.instrument(:field, Gitlab::Graphql::Authorize::Instrumentation.new, after_built_ins: true)
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
deleted file mode 100644
index e8db619f88a..00000000000
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Authorize
- class AuthorizeFieldService
- def initialize(field)
- @field = field
- @old_resolve_proc = @field.resolve_proc
- end
-
- def authorizations?
- authorizations.present?
- end
-
- def authorized_resolve
- proc do |parent_typed_object, args, ctx|
- resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
- authorizing_object = authorize_against(parent_typed_object, resolved_type)
-
- filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
- end
- end
-
- private
-
- def authorizations
- @authorizations ||= (type_authorizations + field_authorizations).uniq
- end
-
- # Returns any authorize metadata from the return type of @field
- def type_authorizations
- type = @field.type
-
- # When the return type of @field is a collection, find the singular type
- if @field.connection?
- type = node_type_for_relay_connection(type)
- elsif type.list?
- type = node_type_for_basic_connection(type)
- end
-
- type = type.unwrap if type.kind.non_null?
-
- Array.wrap(type.metadata[:authorize])
- end
-
- # Returns any authorize metadata from @field
- def field_authorizations
- return [] if @field.metadata[:authorize] == true
-
- Array.wrap(@field.metadata[:authorize])
- end
-
- def authorize_against(parent_typed_object, resolved_type)
- if scalar_type?
- # The field is a built-in/scalar type, or a list of scalars
- # authorize using the parent's object
- parent_typed_object.object
- elsif @field.connection? || @field.type.list? || resolved_type.is_a?(Array)
- # The field is a connection or a list of non-built-in types, we'll
- # authorize each element when rendering
- nil
- elsif resolved_type.respond_to?(:object)
- # The field is a type representing a single object, we'll authorize
- # against the object directly
- resolved_type.object
- else
- # Resolved type is a single object that might not be loaded yet by
- # the batchloader, we'll authorize that
- resolved_type
- end
- end
-
- def filter_allowed(current_user, resolved_type, authorizing_object)
- if resolved_type.nil?
- # We're not rendering anything, for example when a record was not found
- # no need to do anything
- elsif authorizing_object
- # Authorizing fields representing scalars, or a simple field with an object
- ::Gitlab::Graphql::Lazy.with_value(authorizing_object) do |object|
- resolved_type if allowed_access?(current_user, object)
- end
- elsif @field.connection?
- ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |type|
- # A connection with pagination, modify the visible nodes on the
- # connection type in place
- nodes = to_nodes(type)
- nodes.keep_if { |node| allowed_access?(current_user, node) } if nodes
- type
- end
- elsif @field.type.list? || resolved_type.is_a?(Array)
- # A simple list of rendered types each object being an object to authorize
- ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |items|
- items.select do |single_object_type|
- object_type = realized(single_object_type)
- object = object_type.try(:object) || object_type
- allowed_access?(current_user, object)
- end
- end
- else
- raise "Can't authorize #{@field}"
- end
- end
-
- # Ensure that we are dealing with realized objects, not delayed promises
- def realized(thing)
- ::Gitlab::Graphql::Lazy.force(thing)
- end
-
- # Try to get the connection
- # can be at type.object or at type
- def to_nodes(type)
- if type.respond_to?(:nodes)
- type.nodes
- elsif type.respond_to?(:object)
- to_nodes(type.object)
- else
- nil
- end
- end
-
- def allowed_access?(current_user, object)
- object = realized(object)
-
- authorizations.all? do |ability|
- Ability.allowed?(current_user, ability, object)
- end
- end
-
- # Returns the singular type for relay connections.
- # This will be the type class of edges.node
- def node_type_for_relay_connection(type)
- type.unwrap.get_field('edges').type.unwrap.get_field('node').type
- end
-
- # Returns the singular type for basic connections, for example `[Types::ProjectType]`
- def node_type_for_basic_connection(type)
- type.unwrap
- end
-
- def scalar_type?
- node_type_for_basic_connection(@field.type).kind.scalar?
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index 6ee446011d4..4d575b964e5 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -5,15 +5,17 @@ module Gitlab
module Authorize
module AuthorizeResource
extend ActiveSupport::Concern
+ ConfigurationError = Class.new(StandardError)
- RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does " \
+ "not exist or you don't have permission to perform this action"
class_methods do
def required_permissions
# If the `#authorize` call is used on multiple classes, we add the
# permissions specified on a subclass, to the ones that were specified
- # on it's superclass.
- @required_permissions ||= if self.respond_to?(:superclass) && superclass.respond_to?(:required_permissions)
+ # on its superclass.
+ @required_permissions ||= if respond_to?(:superclass) && superclass.respond_to?(:required_permissions)
superclass.required_permissions.dup
else
[]
@@ -23,6 +25,18 @@ module Gitlab
def authorize(*permissions)
required_permissions.concat(permissions)
end
+
+ def authorizes_object?
+ defined?(@authorizes_object) ? @authorizes_object : false
+ end
+
+ def authorizes_object!
+ @authorizes_object = true
+ end
+
+ def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR)
+ raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, msg
+ end
end
def find_object(*args)
@@ -37,33 +51,21 @@ module Gitlab
object
end
+ # authorizes the object using the current class authorization.
def authorize!(object)
- unless authorized_resource?(object)
- raise_resource_not_available_error!
- end
+ raise_resource_not_available_error! unless authorized_resource?(object)
end
- # this was named `#authorized?`, however it conflicts with the native
- # graphql gem version
- # TODO consider adopting the gem's built in authorization system
- # https://gitlab.com/gitlab-org/gitlab/issues/13984
def authorized_resource?(object)
# Sanity check. We don't want to accidentally allow a developer to authorize
# without first adding permissions to authorize against
- if self.class.required_permissions.empty?
- raise Gitlab::Graphql::Errors::ArgumentError, "#{self.class.name} has no authorizations"
- end
+ raise ConfigurationError, "#{self.class.name} has no authorizations" if self.class.authorization.none?
- self.class.required_permissions.all? do |ability|
- # The actions could be performed across multiple objects. In which
- # case the current user is common, and we could benefit from the
- # caching in `DeclarativePolicy`.
- Ability.allowed?(current_user, ability, object, scope: :user)
- end
+ self.class.authorization.ok?(object, current_user)
end
- def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, msg
+ def raise_resource_not_available_error!(*args)
+ self.class.raise_resource_not_available_error!(*args)
end
end
end
diff --git a/lib/gitlab/graphql/authorize/connection_filter_extension.rb b/lib/gitlab/graphql/authorize/connection_filter_extension.rb
new file mode 100644
index 00000000000..20526e19c2a
--- /dev/null
+++ b/lib/gitlab/graphql/authorize/connection_filter_extension.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Authorize
+ class ConnectionFilterExtension < GraphQL::Schema::FieldExtension
+ class Redactor
+ include ::Gitlab::Graphql::Laziness
+
+ def initialize(type, context)
+ @type = type
+ @context = context
+ end
+
+ def redact(nodes)
+ remove_unauthorized(nodes)
+
+ nodes
+ end
+
+ def active?
+ # some scalar types (such as integers) do not respond to :authorized?
+ return false unless @type.respond_to?(:authorized?)
+
+ auth = @type.try(:authorization)
+
+ auth.nil? || auth.any?
+ end
+
+ private
+
+ def remove_unauthorized(nodes)
+ nodes
+ .map! { |lazy| force(lazy) }
+ .keep_if { |forced| @type.authorized?(forced, @context) }
+ end
+ end
+
+ def after_resolve(value:, context:, **rest)
+ if @field.connection?
+ redact_connection(value, context)
+ elsif @field.type.list?
+ redact_list(value.to_a, context) unless value.nil?
+ end
+
+ value
+ end
+
+ def redact_connection(conn, context)
+ redactor = Redactor.new(@field.type.unwrap.node_type, context)
+ return unless redactor.active?
+
+ conn.redactor = redactor if conn.respond_to?(:redactor=)
+ end
+
+ def redact_list(list, context)
+ redactor = Redactor.new(@field.type.unwrap, context)
+ redactor.redact(list) if redactor.active?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
deleted file mode 100644
index 15ecc3b04f0..00000000000
--- a/lib/gitlab/graphql/authorize/instrumentation.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Authorize
- class Instrumentation
- # Replace the resolver for the field with one that will only return the
- # resolved object if the permissions check is successful.
- def instrument(_type, field)
- service = AuthorizeFieldService.new(field)
-
- if service.authorizations?
- field.redefine { resolve(service.authorized_resolve) }
- else
- field
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/authorize/object_authorization.rb b/lib/gitlab/graphql/authorize/object_authorization.rb
new file mode 100644
index 00000000000..5f6c266fd61
--- /dev/null
+++ b/lib/gitlab/graphql/authorize/object_authorization.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Authorize
+ class ObjectAuthorization
+ attr_reader :abilities
+
+ def initialize(abilities)
+ @abilities = Array.wrap(abilities).flatten
+ end
+
+ def none?
+ abilities.empty?
+ end
+
+ def any?
+ abilities.present?
+ end
+
+ def ok?(object, current_user)
+ return true if none?
+
+ abilities.all? do |ability|
+ Ability.allowed?(current_user, ability, object)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 40dee0142b9..e4527f06eff 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -20,7 +20,9 @@ module Gitlab
@blob_content = blob_content
end
- def highlight(text, continue: true, plain: false)
+ def highlight(text, continue: false, plain: false, context: {})
+ @context = context
+
plain ||= text.length > MAXIMUM_TEXT_HIGHLIGHT_SIZE
highlighted_text = highlight_text(text, continue: continue, plain: plain)
@@ -38,6 +40,8 @@ module Gitlab
private
+ attr_reader :context
+
def custom_language
return unless @language
@@ -53,13 +57,13 @@ module Gitlab
end
def highlight_plain(text)
- @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
+ @formatter.format(Rouge::Lexers::PlainText.lex(text), context).html_safe
end
def highlight_rich(text, continue: true)
tag = lexer.tag
tokens = lexer.lex(text, continue: continue)
- Timeout.timeout(timeout_time) { @formatter.format(tokens, tag: tag).html_safe }
+ Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
rescue Timeout::Error => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
highlight_plain(text)
diff --git a/lib/gitlab/usage_data_counters/known_events/epic_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_events.yml
index c478d96b1a9..4b1b12bb58c 100644
--- a/lib/gitlab/usage_data_counters/known_events/epic_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/epic_events.yml
@@ -8,3 +8,9 @@
redis_slot: project_management
aggregation: daily
feature_flag: track_epics_activity
+
+- name: g_project_management_users_destroying_epic_notes
+ category: epics_usage
+ redis_slot: project_management
+ aggregation: daily
+ feature_flag: track_epics_activity
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 8f18d6433e0..e0e9677fac7 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -7,10 +7,11 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
- # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
+ # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
+ # [+line_number+] The line number used to populate line IDs
def initialize(options = {})
- @line_number = 1
@tag = options[:tag]
+ @line_number = options[:line_number] || 1
end
def stream(tokens)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f0add286da8..79def8448fb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4995,6 +4995,9 @@ msgstr ""
msgid "Board|Load more issues"
msgstr ""
+msgid "Bold text"
+msgstr ""
+
msgid "Both project and dashboard_path are required"
msgstr ""
@@ -6347,6 +6350,9 @@ msgstr ""
msgid "Close %{tabname}"
msgstr ""
+msgid "Close design"
+msgstr ""
+
msgid "Close epic"
msgstr ""
@@ -12508,6 +12514,9 @@ msgstr ""
msgid "Expand milestones"
msgstr ""
+msgid "Expand panel"
+msgstr ""
+
msgid "Expand sidebar"
msgstr ""
@@ -13413,6 +13422,9 @@ msgstr ""
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
msgstr ""
+msgid "Focus filter bar"
+msgstr ""
+
msgid "FogBugz Email"
msgstr ""
@@ -17229,6 +17241,9 @@ msgstr ""
msgid "It's you"
msgstr ""
+msgid "Italic text"
+msgstr ""
+
msgid "Iteration"
msgstr ""
@@ -17628,21 +17643,6 @@ msgstr ""
msgid "KeyboardKey|Ctrl+"
msgstr ""
-msgid "KeyboardShortcuts|Commit (when editing commit message)"
-msgstr ""
-
-msgid "KeyboardShortcuts|Global Shortcuts"
-msgstr ""
-
-msgid "KeyboardShortcuts|Toggle GitLab Next"
-msgstr ""
-
-msgid "KeyboardShortcuts|Toggle the Performance Bar"
-msgstr ""
-
-msgid "KeyboardShortcuts|Web IDE"
-msgstr ""
-
msgid "Keys"
msgstr ""
@@ -18337,6 +18337,9 @@ msgstr ""
msgid "Link copied"
msgstr ""
+msgid "Link text"
+msgstr ""
+
msgid "Link title"
msgstr ""
@@ -19889,6 +19892,9 @@ msgstr ""
msgid "Mirroring will only be available if the feature is included in the plan of the selected group or user."
msgstr ""
+msgid "Miscellaneous"
+msgstr ""
+
msgid "Missing"
msgstr ""
@@ -20627,6 +20633,9 @@ msgstr ""
msgid "Next commit"
msgstr ""
+msgid "Next design"
+msgstr ""
+
msgid "Next file in diff"
msgstr ""
@@ -23128,6 +23137,9 @@ msgstr ""
msgid "Previous commit"
msgstr ""
+msgid "Previous design"
+msgstr ""
+
msgid "Previous file in diff"
msgstr ""
@@ -27739,7 +27751,7 @@ msgstr ""
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
-msgid "SetStatusModal|\"Busy\" will be shown next to your name"
+msgid "SetStatusModal|A busy indicator is shown next to your name and avatar."
msgstr ""
msgid "SetStatusModal|Add status emoji"
@@ -27751,6 +27763,9 @@ msgstr ""
msgid "SetStatusModal|Clear status"
msgstr ""
+msgid "SetStatusModal|Clear status after"
+msgstr ""
+
msgid "SetStatusModal|Edit status"
msgstr ""
@@ -27772,6 +27787,9 @@ msgstr ""
msgid "SetStatusModal|What's your status?"
msgstr ""
+msgid "SetStatusModal|Your status resets on %{date}."
+msgstr ""
+
msgid "Sets %{epic_ref} as parent epic."
msgstr ""
@@ -31768,6 +31786,9 @@ msgstr ""
msgid "Toggle focus mode"
msgstr ""
+msgid "Toggle keyboard shortcuts help dialog"
+msgstr ""
+
msgid "Toggle navigation"
msgstr ""
@@ -33415,6 +33436,9 @@ msgstr ""
msgid "View log"
msgstr ""
+msgid "View logs"
+msgstr ""
+
msgid "View merge request"
msgstr ""
diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index 4d55eee2ffa..e430e18b76a 100644
--- a/spec/frontend/notes/components/discussion_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -2,6 +2,11 @@
import 'mousetrap';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
+import {
+ keysFor,
+ MR_NEXT_UNRESOLVED_DISCUSSION,
+ MR_PREVIOUS_UNRESOLVED_DISCUSSION,
+} from '~/behaviors/shortcuts/keybindings';
import DiscussionNavigator from '~/notes/components/discussion_navigator.vue';
import eventHub from '~/notes/event_hub';
@@ -60,13 +65,13 @@ describe('notes/components/discussion_navigator', () => {
});
it('calls jumpToNextDiscussion when pressing `n`', () => {
- Mousetrap.trigger('n');
+ Mousetrap.trigger(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION));
expect(jumpToNextDiscussion).toHaveBeenCalled();
});
it('calls jumpToPreviousDiscussion when pressing `p`', () => {
- Mousetrap.trigger('p');
+ Mousetrap.trigger(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION));
expect(jumpToPreviousDiscussion).toHaveBeenCalled();
});
@@ -87,8 +92,8 @@ describe('notes/components/discussion_navigator', () => {
});
it('unbinds keys', () => {
- expect(Mousetrap.unbind).toHaveBeenCalledWith('n');
- expect(Mousetrap.unbind).toHaveBeenCalledWith('p');
+ expect(Mousetrap.unbind).toHaveBeenCalledWith(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION));
+ expect(Mousetrap.unbind).toHaveBeenCalledWith(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION));
});
it('unbinds event hub listeners', () => {
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 21b9721438d..403f9509f84 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -44,6 +44,7 @@ describe('SetStatusModalWrapper', () => {
const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
const initModal = ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
@@ -57,18 +58,18 @@ describe('SetStatusModalWrapper', () => {
return wrapper.vm.$nextTick();
};
- beforeEach(async () => {
- mockEmoji = await initEmojiMock();
- wrapper = createComponent();
- return initModal();
- });
-
afterEach(() => {
wrapper.destroy();
mockEmoji.restore();
});
describe('with minimum props', () => {
+ beforeEach(async () => {
+ mockEmoji = await initEmojiMock();
+ wrapper = createComponent();
+ return initModal();
+ });
+
it('sets the hidden status emoji field', () => {
const field = findFormField('emoji');
expect(field.exists()).toBe(true);
@@ -96,6 +97,14 @@ describe('SetStatusModalWrapper', () => {
findToggleEmojiButton().trigger('click');
expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
});
+
+ it('displays the clear status at dropdown', () => {
+ expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
+ });
+
+ it('does not display the clear status at message', () => {
+ expect(findClearStatusAtMessage().exists()).toBe(false);
+ });
});
describe('with no currentMessage set', () => {
@@ -146,9 +155,28 @@ describe('SetStatusModalWrapper', () => {
});
});
+ describe('with currentClearStatusAfter set', () => {
+ beforeEach(async () => {
+ mockEmoji = await initEmojiMock();
+ wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' });
+ return initModal();
+ });
+
+ it('displays the clear status at message', () => {
+ const clearStatusAtMessage = findClearStatusAtMessage();
+
+ expect(clearStatusAtMessage.exists()).toBe(true);
+ expect(clearStatusAtMessage.text()).toBe('Your status resets on 2021-01-01 00:00:00 UTC.');
+ });
+ });
+
describe('update status', () => {
describe('succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ mockEmoji = await initEmojiMock();
+ wrapper = createComponent();
+ await initModal();
+
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
});
@@ -167,18 +195,26 @@ describe('SetStatusModalWrapper', () => {
// set the availability status
findAvailabilityCheckbox().vm.$emit('input', true);
+ // set the currentClearStatusAfter to 30 minutes
+ wrapper.find('[data-testid="thirtyMinutes"]').vm.$emit('click');
+
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
- const commonParams = { emoji: defaultEmoji, message: defaultMessage };
+ const commonParams = {
+ emoji: defaultEmoji,
+ message: defaultMessage,
+ };
expect(UserApi.updateUserStatus).toHaveBeenCalledTimes(2);
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(1, {
availability: AVAILABILITY_STATUS.NOT_SET,
+ clearStatusAfter: null,
...commonParams,
});
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(2, {
availability: AVAILABILITY_STATUS.BUSY,
+ clearStatusAfter: '30_minutes',
...commonParams,
});
});
@@ -208,7 +244,11 @@ describe('SetStatusModalWrapper', () => {
});
describe('with errors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ mockEmoji = await initEmojiMock();
+ wrapper = createComponent();
+ await initModal();
+
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index 33b11e1ca09..7ab5adadd1f 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Gitlab::Graphql::Authorize' do
+RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
include GraphqlHelpers
include Graphql::ResolverFactories
@@ -10,10 +10,14 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:permission_single) { :foo }
let(:permission_collection) { [:foo, :bar] }
let(:test_object) { double(name: 'My name') }
+ let(:authorizing_object) { test_object }
+ # to override when combining permissions
+ let(:permission_object_one) { authorizing_object }
+ let(:permission_object_two) { authorizing_object }
+
let(:query_string) { '{ item { name } }' }
let(:result) do
schema = empty_schema
- schema.use(Gitlab::Graphql::Authorize)
execute_query(query_type, schema: schema)
end
@@ -33,18 +37,25 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
shared_examples 'authorization with a collection of permissions' do
it 'returns the protected field when user has all permissions' do
- permit(*permission_collection)
+ permit_on(permission_object_one, permission_collection.first)
+ permit_on(permission_object_two, permission_collection.second)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user only has one of the permissions' do
- permit(permission_collection.first)
+ permit_on(permission_object_one, permission_collection.first)
+
+ expect(subject).to be_nil
+ end
+
+ it 'returns nil when user only has the other of the permissions' do
+ permit_on(permission_object_two, permission_collection.second)
expect(subject).to be_nil
end
- it 'returns nil when user only has none of the permissions' do
+ it 'returns nil when user has neither of the required permissions' do
expect(subject).to be_nil
end
end
@@ -56,6 +67,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
describe 'Field authorizations' do
let(:type) { type_factory }
+ let(:authorizing_object) { nil }
describe 'with a single permission' do
let(:query_type) do
@@ -71,9 +83,10 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:query_type) do
permissions = permission_collection
query_factory do |qt|
- qt.field :item, type, null: true, resolver: new_resolver(test_object) do
- authorize permissions
- end
+ qt.field :item, type,
+ null: true,
+ resolver: new_resolver(test_object),
+ authorize: permissions
end
end
@@ -110,9 +123,8 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:type) do
permissions = permission_collection
type_factory do |type|
- type.field :name, GraphQL::STRING_TYPE, null: true do
- authorize permissions
- end
+ type.field :name, GraphQL::STRING_TYPE, null: true,
+ authorize: permissions
end
end
@@ -163,6 +175,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
end
describe 'type and field authorizations together' do
+ let(:authorizing_object) { anything }
let(:permission_1) { permission_collection.first }
let(:permission_2) { permission_collection.last }
@@ -181,7 +194,62 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
include_examples 'authorization with a collection of permissions'
end
- describe 'type authorizations when applied to a relay connection' do
+ describe 'resolver and field authorizations together' do
+ let(:permission_1) { permission_collection.first }
+ let(:permission_2) { permission_collection.last }
+ let(:type) { type_factory }
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :item, type, null: true,
+ resolver: resolver,
+ authorize: permission_2
+ end
+ end
+
+ context 'when the resolver authorizes the object' do
+ let(:permission_object_one) { be_nil }
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = simple_resolver(test_object)
+ resolver.include(::Gitlab::Graphql::Authorize::AuthorizeResource)
+ resolver.authorize permission_1
+ resolver.authorizes_object!
+ resolver
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+
+ context 'when the resolver does not authorize the object, but instead calls authorized_find!' do
+ let(:permission_object_one) { test_object }
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = new_resolver(test_object, method: :find_object)
+ resolver.authorize permission_1
+ resolver
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+
+ context 'when the resolver calls authorized_find!, but does not list any permissions' do
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = new_resolver(test_object, method: :find_object)
+ resolver
+ end
+
+ it 'raises a configuration error' do
+ permit_on(permission_object_two, permission_collection.second)
+
+ expect { execute_query(query_type) }
+ .to raise_error(::Gitlab::Graphql::Authorize::AuthorizeResource::ConfigurationError)
+ end
+ end
+ end
+
+ describe 'when type authorizations when applied to a relay connection' do
let(:query_string) { '{ item { edges { node { name } } } }' }
let(:second_test_object) { double(name: 'Second thing') }
@@ -303,8 +371,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
private
def permit(*permissions)
+ permit_on(authorizing_object, *permissions)
+ end
+
+ def permit_on(object, *permissions)
permissions.each do |permission|
- allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, permission, object).and_return(true)
end
end
end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index cb2bb25b098..4ddd74b1eb7 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -14,10 +14,6 @@ RSpec.describe GitlabSchema do
expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::GenericTracing))
end
- it 'enables the authorization instrumenter' do
- expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation))
- end
-
it 'has the base mutation' do
expect(described_class.mutation).to eq(::Types::MutationType)
end
diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 24104a20465..dd9305d2197 100644
--- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::Boards::Issues::IssueMoveList do
+ include GraphqlHelpers
+
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:board) { create(:board, group: group) }
@@ -16,9 +18,8 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) }
- let(:current_user) { user }
- let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
- let(:params) { { board: board, project_path: project.full_path, iid: issue1.iid } }
+ let(:current_ctx) { { current_user: user } }
+ let(:params) { { board_id: global_id_of(board), project_path: project.full_path, iid: issue1.iid } }
let(:move_params) do
{
from_list_id: list1.id,
@@ -33,26 +34,45 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
group.add_guest(guest)
end
- subject do
- mutation.resolve(**params.merge(move_params))
- end
+ describe '#resolve' do
+ subject do
+ sync(resolve(described_class, args: params.merge(move_params), ctx: current_ctx))
+ end
+
+ %i[from_list_id to_list_id].each do |arg_name|
+ context "when we only pass #{arg_name}" do
+ let(:move_params) { { arg_name => list1.id } }
- describe '#ready?' do
- it 'raises an error if required arguments are missing' do
- expect { mutation.ready?(**params) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \
- "fromListId, toListId, afterId or beforeId is required")
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ 'Both fromListId and toListId must be present'
+ )
+ end
+ end
end
- it 'raises an error if only one of fromListId and toListId is present' do
- expect { mutation.ready?(**params.merge(from_list_id: list1.id)) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError,
- 'Both fromListId and toListId must be present'
+ context 'when required arguments are missing' do
+ let(:move_params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ "At least one of the arguments fromListId, toListId, afterId or beforeId is required"
)
+ end
+ end
+
+ context 'when the board ID is wrong' do
+ before do
+ params[:board_id] = global_id_of(project)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(::GraphQL::LoadApplicationObjectFailedError)
+ end
end
- end
- describe '#resolve' do
context 'when user have access to resources' do
it 'moves and repositions issue' do
subject
@@ -63,15 +83,11 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
end
end
- context 'when user have no access to resources' do
- shared_examples 'raises a resource not available error' do
- it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
- end
-
- context 'when user cannot update issue' do
- let(:current_user) { guest }
+ context 'when user cannot update issue' do
+ let(:current_ctx) { { current_user: guest } }
- it_behaves_like 'raises a resource not available error'
+ specify do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb
index 326d88cea80..e92000194b1 100644
--- a/spec/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/graphql/mutations/design_management/upload_spec.rb
@@ -32,6 +32,10 @@ RSpec.describe Mutations::DesignManagement::Upload do
end
context "when the feature is not available" do
+ before do
+ enable_design_management(false)
+ end
+
it_behaves_like "resource not available"
end
@@ -99,20 +103,20 @@ RSpec.describe Mutations::DesignManagement::Upload do
it_behaves_like "resource not available"
end
- context "a valid design" do
+ context "with a valid design" do
it "returns the updated designs" do
expect(resolve[:errors]).to eq []
expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png")
end
end
- context "context when passing an invalid project" do
+ context "when passing an invalid project" do
let(:project) { build(:project) }
it_behaves_like "resource not available"
end
- context "context when passing an invalid issue" do
+ context "when passing an invalid issue" do
let(:issue) { build(:issue) }
it_behaves_like "resource not available"
diff --git a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
index 27ac1572cab..4c244da5c62 100644
--- a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
+++ b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
@@ -38,11 +38,8 @@ RSpec.describe LooksAhead do
user = Class.new(GraphQL::Schema::Object) do
graphql_name 'User'
field :name, String, null: true
- field :issues, issue.connection_type,
- null: true
- field :issues_with_lookahead, issue.connection_type,
- resolver: issues_resolver,
- null: true
+ field :issues, issue.connection_type, null: true
+ field :issues_with_lookahead, issue.connection_type, resolver: issues_resolver, null: true
end
Class.new(GraphQL::Schema) do
@@ -101,7 +98,7 @@ RSpec.describe LooksAhead do
expect(res['errors']).to be_blank
expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name)
- %w(issues issuesWithLookahead).each do |field|
+ %w[issues issuesWithLookahead].each do |field|
expect(all_issue_titles(res, field)).to match_array(issue_titles)
expect(all_label_ids(res, field)).to match_array(expected_label_ids)
end
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 7dd968d90a8..5e2a075c5b3 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
# AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC
# SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2
# SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2
- let(:queries_per_project) { 3 }
+ let(:queries_per_project) { 4 }
context 'no arguments' do
it 'returns all merge requests' do
diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
index b10c2a2ab2a..7e4110af9a4 100644
--- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
+++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
@@ -48,15 +48,21 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
end
end
- context 'without project' do
- let_it_be(:integration) { create(:prometheus_service, project: nil, group: create(:group)) }
-
- it_behaves_like 'has field with value', 'token' do
- let(:value) { nil }
- end
-
- it_behaves_like 'has field with value', 'url' do
- let(:value) { nil }
+ context 'group integration' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) }
+
+ # Since it is impossible to authorize the parent here, given that the
+ # project is nil, all fields should be redacted:
+
+ described_class.fields.keys.each do |field_name|
+ context "field: #{field_name}" do
+ it 'is redacted' do
+ expect do
+ resolve_field(field_name, integration, current_user: user)
+ end.to raise_error(GraphqlHelpers::UnauthorizedObject)
+ end
+ end
end
end
end
diff --git a/spec/graphql/types/base_object_spec.rb b/spec/graphql/types/base_object_spec.rb
new file mode 100644
index 00000000000..d144c1f6456
--- /dev/null
+++ b/spec/graphql/types/base_object_spec.rb
@@ -0,0 +1,434 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::BaseObject do
+ include GraphqlHelpers
+
+ describe 'scoping items' do
+ let_it_be(:custom_auth) do
+ Class.new(::Gitlab::Graphql::Authorize::ObjectAuthorization) do
+ def any?
+ true
+ end
+
+ def ok?(object, _current_user)
+ return false if object == { id: 100 }
+ return false if object.try(:deactivated?)
+
+ true
+ end
+ end
+ end
+
+ let_it_be(:test_schema) do
+ auth = custom_auth.new(nil)
+
+ base_object = Class.new(described_class) do
+ # Override authorization so we don't need to mock Ability
+ define_singleton_method :authorization do
+ auth
+ end
+ end
+
+ y_type = Class.new(base_object) do
+ graphql_name 'Y'
+ authorize :read_y
+ field :id, Integer, null: false
+
+ def id
+ object[:id]
+ end
+ end
+
+ number_type = Module.new do
+ include ::Types::BaseInterface
+
+ graphql_name 'Number'
+
+ field :value, Integer, null: false
+ end
+
+ odd_type = Class.new(described_class) do
+ graphql_name 'Odd'
+ implements number_type
+
+ authorize :read_odd
+ field :odd_value, Integer, null: false
+
+ def odd_value
+ object[:value]
+ end
+ end
+
+ even_type = Class.new(described_class) do
+ graphql_name 'Even'
+ implements number_type
+
+ authorize :read_even
+ field :even_value, Integer, null: false
+
+ def even_value
+ object[:value]
+ end
+ end
+
+ # an abstract type, delegating authorization to members
+ odd_or_even = Class.new(::Types::BaseUnion) do
+ graphql_name 'OddOrEven'
+
+ possible_types odd_type, even_type
+
+ define_singleton_method :resolve_type do |object, ctx|
+ if object[:value].odd?
+ odd_type
+ else
+ even_type
+ end
+ end
+ end
+
+ number_type.define_singleton_method :resolve_type do |object, ctx|
+ odd_or_even.resolve_type(object, ctx)
+ end
+
+ x_type = Class.new(base_object) do
+ graphql_name 'X'
+ # Scalar types
+ field :title, String, null: true
+ # monomorphic types
+ field :lazy_list_of_ys, [y_type], null: true
+ field :list_of_lazy_ys, [y_type], null: true
+ field :array_ys_conn, y_type.connection_type, null: true
+ # polymorphic types
+ field :polymorphic_conn, odd_or_even.connection_type, null: true
+ field :polymorphic_object, odd_or_even, null: true do
+ argument :value, Integer, required: true
+ end
+ field :interface_conn, number_type.connection_type, null: true
+
+ def lazy_list_of_ys
+ ::Gitlab::Graphql::Lazy.new { object[:ys] }
+ end
+
+ def list_of_lazy_ys
+ object[:ys].map { |y| ::Gitlab::Graphql::Lazy.new { y } }
+ end
+
+ def array_ys_conn
+ object[:ys].dup
+ end
+
+ def polymorphic_conn
+ object[:values].dup
+ end
+ alias_method :interface_conn, :polymorphic_conn
+
+ def polymorphic_object(value)
+ value
+ end
+ end
+
+ user_type = Class.new(base_object) do
+ graphql_name 'User'
+ authorize :read_user
+ field 'name', String, null: true
+ end
+
+ Class.new(GraphQL::Schema) do
+ lazy_resolve ::Gitlab::Graphql::Lazy, :force
+ use ::GraphQL::Pagination::Connections
+ use ::Gitlab::Graphql::Pagination::Connections
+
+ query(Class.new(::Types::BaseObject) do
+ graphql_name 'Query'
+ field :x, x_type, null: true
+ field :users, user_type.connection_type, null: true
+
+ def x
+ ::Gitlab::Graphql::Lazy.new { context[:x] }
+ end
+
+ def users
+ ::Gitlab::Graphql::Lazy.new { User.id_in(context[:user_ids]).order(id: :asc) }
+ end
+ end)
+
+ def unauthorized_object(err)
+ nil
+ end
+ end
+ end
+
+ def document(path)
+ GraphQL.parse(<<~GQL)
+ query {
+ x {
+ title
+ #{query_graphql_path(path, 'id')}
+ }
+ }
+ GQL
+ end
+
+ let(:data) do
+ {
+ x: {
+ title: 'Hey',
+ ys: [{ id: 1 }, { id: 100 }, { id: 2 }]
+ }
+ }
+ end
+
+ shared_examples 'array member redaction' do |path|
+ let(:result) do
+ query = GraphQL::Query.new(test_schema, document: document(path), context: data)
+ query.result.to_h
+ end
+
+ it 'redacts the unauthorized array member' do
+ expect(graphql_dig_at(result, 'data', 'x', 'title')).to eq('Hey')
+ expect(graphql_dig_at(result, 'data', 'x', *path)).to contain_exactly(
+ eq({ 'id' => 1 }),
+ eq({ 'id' => 2 })
+ )
+ end
+ end
+
+ # For example a batchloaded association
+ context 'a lazy list' do
+ it_behaves_like 'array member redaction', %w[lazyListOfYs]
+ end
+
+ # For example using a batchloader to map over a set of IDs
+ context 'a list of lazy items' do
+ it_behaves_like 'array member redaction', %w[listOfLazyYs]
+ end
+
+ context 'an array connection of items' do
+ it_behaves_like 'array member redaction', %w[arrayYsConn nodes]
+ end
+
+ context 'an array connection of items, selecting edges' do
+ it_behaves_like 'array member redaction', %w[arrayYsConn edges node]
+ end
+
+ it 'paginates arrays correctly' do
+ n = 7
+
+ data = {
+ x: {
+ ys: (95..105).to_a.map { |id| { id: id } }
+ }
+ }
+
+ doc = ->(after) do
+ GraphQL.parse(<<~GQL)
+ query {
+ x {
+ ys: arrayYsConn(#{attributes_to_graphql(first: n, after: after)}) {
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ }
+ nodes { id }
+ }
+ }
+ }
+ GQL
+ end
+ returned_items = ->(ids) do
+ ids.to_a.map { |id| eq({ 'id' => id }) }
+ end
+
+ query = GraphQL::Query.new(test_schema, document: doc[nil], context: data)
+ result = query.result.to_h
+
+ ys = result.dig('data', 'x', 'ys', 'nodes')
+ page = result.dig('data', 'x', 'ys', 'pageInfo')
+ # We expect this page to be smaller, since we paginate before redaction
+ expect(ys).to match_array(returned_items[(95..101).to_a - [100]])
+ expect(page).to include('hasNextPage' => true, 'hasPreviousPage' => false)
+
+ cursor = page['endCursor']
+ query_2 = GraphQL::Query.new(test_schema, document: doc[cursor], context: data)
+ result_2 = query_2.result.to_h
+
+ ys = result_2.dig('data', 'x', 'ys', 'nodes')
+ page = result_2.dig('data', 'x', 'ys', 'pageInfo')
+ expect(ys).to match_array(returned_items[102..105])
+ expect(page).to include('hasNextPage' => false, 'hasPreviousPage' => true)
+ end
+
+ it 'filters connections correctly' do
+ active_users = create_list(:user, 3, state: :active)
+ inactive = create(:user, state: :deactivated)
+
+ data = { user_ids: [inactive, *active_users].map(&:id) }
+
+ doc = GraphQL.parse(<<~GQL)
+ query {
+ users { nodes { name } }
+ }
+ GQL
+
+ query = GraphQL::Query.new(test_schema, document: doc, context: data)
+ result = query.result.to_h
+
+ expect(result.dig('data', 'users', 'nodes')).to match_array(active_users.map do |u|
+ eq({ 'name' => u.name })
+ end)
+ end
+
+ it 'filters polymorphic connections' do
+ data = {
+ current_user: :the_user,
+ x: {
+ values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }]
+ }
+ }
+
+ doc = GraphQL.parse(<<~GQL)
+ query {
+ x {
+ things: polymorphicConn {
+ nodes {
+ ... on Odd { oddValue }
+ ... on Even { evenValue }
+ }
+ }
+ }
+ }
+ GQL
+
+ # Each ability check happens twice: once in the collection, and once
+ # on the type. We expect the ability checks to be cached.
+ expect(Ability).to receive(:allowed?).twice
+ .with(:the_user, :read_odd, { value: 1 }).and_return(true)
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_odd, { value: 3 }).and_return(false)
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_even, { value: 2 }).and_return(false)
+ expect(Ability).to receive(:allowed?).twice
+ .with(:the_user, :read_even, { value: 4 }).and_return(true)
+
+ query = GraphQL::Query.new(test_schema, document: doc, context: data)
+ result = query.result.to_h
+
+ things = result.dig('data', 'x', 'things', 'nodes')
+
+ expect(things).to contain_exactly(
+ { 'oddValue' => 1 },
+ { 'evenValue' => 4 }
+ )
+ end
+
+ it 'filters interface connections' do
+ data = {
+ current_user: :the_user,
+ x: {
+ values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }]
+ }
+ }
+
+ doc = GraphQL.parse(<<~GQL)
+ query {
+ x {
+ things: interfaceConn {
+ nodes {
+ value
+ ... on Odd { oddValue }
+ ... on Even { evenValue }
+ }
+ }
+ }
+ }
+ GQL
+
+ # Each ability check happens twice: once in the collection, and once
+ # on the type. We expect the ability checks to be cached.
+ expect(Ability).to receive(:allowed?).twice
+ .with(:the_user, :read_odd, { value: 1 }).and_return(true)
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_odd, { value: 3 }).and_return(false)
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_even, { value: 2 }).and_return(false)
+ expect(Ability).to receive(:allowed?).twice
+ .with(:the_user, :read_even, { value: 4 }).and_return(true)
+
+ query = GraphQL::Query.new(test_schema, document: doc, context: data)
+ result = query.result.to_h
+
+ things = result.dig('data', 'x', 'things', 'nodes')
+
+ expect(things).to contain_exactly(
+ { 'value' => 1, 'oddValue' => 1 },
+ { 'value' => 4, 'evenValue' => 4 }
+ )
+ end
+
+ it 'redacts polymorphic objects' do
+ data = {
+ current_user: :the_user,
+ x: {
+ values: [{ value: 1 }]
+ }
+ }
+
+ doc = GraphQL.parse(<<~GQL)
+ query {
+ x {
+ ok: polymorphicObject(value: 1) {
+ ... on Odd { oddValue }
+ ... on Even { evenValue }
+ }
+ bad: polymorphicObject(value: 3) {
+ ... on Odd { oddValue }
+ ... on Even { evenValue }
+ }
+ }
+ }
+ GQL
+
+ # Each ability check happens twice: once in the collection, and once
+ # on the type. We expect the ability checks to be cached.
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_odd, { value: 1 }).and_return(true)
+ expect(Ability).to receive(:allowed?).once
+ .with(:the_user, :read_odd, { value: 3 }).and_return(false)
+
+ query = GraphQL::Query.new(test_schema, document: doc, context: data)
+ result = query.result.to_h
+
+ expect(result.dig('data', 'x', 'ok')).to eq({ 'oddValue' => 1 })
+ expect(result.dig('data', 'x', 'bad')).to be_nil
+ end
+
+ it 'paginates before scoping' do
+ # Inactive first so they sort first
+ n = 3
+ inactive = create_list(:user, n - 1, state: :deactivated)
+ active_users = create_list(:user, 2, state: :active)
+
+ data = { user_ids: [*inactive, *active_users].map(&:id) }
+
+ doc = GraphQL.parse(<<~GQL)
+ query {
+ users(first: #{n}) {
+ pageInfo { hasNextPage }
+ nodes { name } }
+ }
+ GQL
+
+ query = GraphQL::Query.new(test_schema, document: doc, context: data)
+ result = query.result.to_h
+
+ # We expect the page to be loaded and then filtered - i.e. to have all
+ # deactivated users removed.
+ expect(result.dig('data', 'users', 'pageInfo', 'hasNextPage')).to be_truthy
+ expect(result.dig('data', 'users', 'nodes'))
+ .to contain_exactly({ 'name' => active_users.first.name })
+ end
+ end
+end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 99cdee6dbb2..d03e39f2051 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -223,39 +223,37 @@ RSpec.describe PageLayoutHelper do
end
describe '#user_status_properties' do
- using RSpec::Parameterized::TableSyntax
-
let(:user) { build(:user) }
- availability_types = Types::AvailabilityEnum.enum
-
- where(:message, :emoji, :availability) do
- "Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
- "Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
- "Some message" | "basketball" | availability_types[:busy]
- "Some message" | "basketball" | availability_types[:not_set]
- "Some message" | "" | availability_types[:busy]
- "Some message" | "" | availability_types[:not_set]
- "" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
- "" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
- "" | "basketball" | availability_types[:busy]
- "" | "basketball" | availability_types[:not_set]
- "" | "" | availability_types[:busy]
- "" | "" | availability_types[:not_set]
- end
+ subject { helper.user_status_properties(user) }
- with_them do
- it "sets the default user status fields" do
- user.status = UserStatus.new(message: message, emoji: emoji, availability: availability)
- result = {
+ context 'when the user has no status' do
+ it 'returns default properties' do
+ is_expected.to eq({
+ current_emoji: '',
+ current_message: '',
can_set_user_availability: true,
- current_availability: availability,
- current_emoji: emoji,
- current_message: message,
default_emoji: UserStatus::DEFAULT_EMOJI
- }
+ })
+ end
+ end
+
+ context 'when user has a status' do
+ let(:time) { 3.hours.ago }
- expect(helper.user_status_properties(user)).to eq(result)
+ before do
+ user.status = UserStatus.new(message: 'Some message', emoji: 'basketball', availability: 'busy', clear_status_at: time)
+ end
+
+ it 'merges the status properties with the defaults' do
+ is_expected.to eq({
+ current_clear_status_after: time.to_s,
+ current_availability: 'busy',
+ current_emoji: 'basketball',
+ current_message: 'Some message',
+ can_set_user_availability: true,
+ default_emoji: UserStatus::DEFAULT_EMOJI
+ })
end
end
end
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index 8d29b001f8d..4c56911e665 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -238,26 +238,36 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
subject { cache.key }
it 'returns cache key' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true")
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:true")
end
- context 'when feature flag is disabled' do
+ context 'when the `introduce_marker_ranges` feature flag is disabled' do
before do
stub_feature_flags(introduce_marker_ranges: false)
end
it 'returns the original version of the cache' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true")
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true:true")
end
end
- context 'when use marker ranges feature flag is disabled' do
+ context 'when the `use_marker_ranges` feature flag is disabled' do
before do
stub_feature_flags(use_marker_ranges: false)
end
it 'returns the original version of the cache' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false")
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false:true")
+ end
+ end
+
+ context 'when the `diff_line_syntax_highlighting` feature flag is disabled' do
+ before do
+ stub_feature_flags(diff_line_syntax_highlighting: false)
+ end
+
+ it 'returns the original version of the cache' do
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:false")
end
end
end
diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb
index a40cd99f6f8..949def599ae 100644
--- a/spec/lib/gitlab/diff/line_spec.rb
+++ b/spec/lib/gitlab/diff/line_spec.rb
@@ -45,6 +45,29 @@ RSpec.describe Gitlab::Diff::Line do
end
end
+ describe '#text' do
+ let(:line) { described_class.new(raw_diff, 'new', 0, 0, 0) }
+ let(:raw_diff) { '+Hello' }
+
+ it 'returns raw diff text' do
+ expect(line.text).to eq('+Hello')
+ end
+
+ context 'when prefix is disabled' do
+ it 'returns raw diff text without prefix' do
+ expect(line.text(prefix: false)).to eq('Hello')
+ end
+
+ context 'when diff is empty' do
+ let(:raw_diff) { '' }
+
+ it 'returns an empty raw diff' do
+ expect(line.text(prefix: false)).to eq('')
+ end
+ end
+ end
+ end
+
context "when setting rich text" do
it 'escapes any HTML special characters in the diff chunk header' do
subject = described_class.new("<input>", "", 0, 0, 0)
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
deleted file mode 100644
index c88506899cd..00000000000
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ /dev/null
@@ -1,253 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# Also see spec/graphql/features/authorization_spec.rb for
-# integration tests of AuthorizeFieldService
-RSpec.describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
- def type(type_authorizations = [])
- Class.new(Types::BaseObject) do
- graphql_name 'TestType'
-
- authorize type_authorizations
- end
- end
-
- def type_with_field(field_type, field_authorizations = [], resolved_value = 'Resolved value', **options)
- Class.new(Types::BaseObject) do
- graphql_name 'TestTypeWithField'
- options.reverse_merge!(null: true)
- field :test_field, field_type,
- authorize: field_authorizations,
- **options
-
- define_method :test_field do
- resolved_value
- end
- end
- end
-
- def resolve
- service.authorized_resolve[type_instance, {}, context]
- end
-
- subject(:service) { described_class.new(field) }
-
- describe '#authorized_resolve' do
- let_it_be(:current_user) { build(:user) }
- let_it_be(:presented_object) { 'presented object' }
- let_it_be(:query_type) { GraphQL::ObjectType.new }
- let_it_be(:schema) { GitlabSchema }
- let_it_be(:query) { GraphQL::Query.new(schema, document: nil, context: {}, variables: {}) }
- let_it_be(:context) { GraphQL::Query::Context.new(query: query, values: { current_user: current_user }, object: nil) }
-
- let(:type_class) { type_with_field(custom_type, :read_field, presented_object) }
- let(:type_instance) { type_class.authorized_new(presented_object, context) }
- let(:field) { type_class.fields['testField'].to_graphql }
-
- subject(:resolved) { ::Gitlab::Graphql::Lazy.force(resolve) }
-
- context 'reading the field of a lazy value' do
- let(:ability) { :read_field }
- let(:presented_object) { lazy_upcase('a') }
- let(:type_class) { type_with_field(GraphQL::STRING_TYPE, ability) }
-
- let(:upcaser) do
- Module.new do
- def self.upcase(strs)
- strs.map(&:upcase)
- end
- end
- end
-
- def lazy_upcase(str)
- ::BatchLoader::GraphQL.for(str).batch do |strs, found|
- strs.zip(upcaser.upcase(strs)).each { |s, us| found[s, us] }
- end
- end
-
- it 'does not run authorizations until we force the resolved value' do
- expect(Ability).not_to receive(:allowed?)
-
- expect(resolve).to respond_to(:force)
- end
-
- it 'runs authorizations when we force the resolved value' do
- spy_ability_check_for(ability, 'A')
-
- expect(resolved).to eq('Resolved value')
- end
-
- it 'redacts values that fail the permissions check' do
- spy_ability_check_for(ability, 'A', passed: false)
-
- expect(resolved).to be_nil
- end
-
- context 'we batch two calls' do
- def resolve(value)
- instance = type_class.authorized_new(lazy_upcase(value), context)
- service.authorized_resolve[instance, {}, context]
- end
-
- it 'batches resolution, but authorizes each object separately' do
- expect(upcaser).to receive(:upcase).once.and_call_original
- spy_ability_check_for(:read_field, 'A', passed: true)
- spy_ability_check_for(:read_field, 'B', passed: false)
- spy_ability_check_for(:read_field, 'C', passed: true)
-
- a = resolve('a')
- b = resolve('b')
- c = resolve('c')
-
- expect(a.force).to be_present
- expect(b.force).to be_nil
- expect(c.force).to be_present
- end
- end
- end
-
- shared_examples 'authorizing fields' do
- context 'scalar types' do
- shared_examples 'checking permissions on the presented object' do
- it 'checks the abilities on the object being presented and returns the value' do
- expected_permissions.each do |permission|
- spy_ability_check_for(permission, presented_object, passed: true)
- end
-
- expect(resolved).to eq('Resolved value')
- end
-
- it 'returns nil if the value was not authorized' do
- allow(Ability).to receive(:allowed?).and_return false
-
- expect(resolved).to be_nil
- end
- end
-
- context 'when the field is a built-in scalar type' do
- let(:type_class) { type_with_field(GraphQL::STRING_TYPE, :read_field) }
- let(:expected_permissions) { [:read_field] }
-
- it_behaves_like 'checking permissions on the presented object'
- end
-
- context 'when the field is a list of scalar types' do
- let(:type_class) { type_with_field([GraphQL::STRING_TYPE], :read_field) }
- let(:expected_permissions) { [:read_field] }
-
- it_behaves_like 'checking permissions on the presented object'
- end
-
- context 'when the field is sub-classed scalar type' do
- let(:type_class) { type_with_field(Types::TimeType, :read_field) }
- let(:expected_permissions) { [:read_field] }
-
- it_behaves_like 'checking permissions on the presented object'
- end
-
- context 'when the field is a list of sub-classed scalar types' do
- let(:type_class) { type_with_field([Types::TimeType], :read_field) }
- let(:expected_permissions) { [:read_field] }
-
- it_behaves_like 'checking permissions on the presented object'
- end
- end
-
- context 'when the field is a connection' do
- context 'when it resolves to nil' do
- let(:type_class) { type_with_field(Types::QueryType.connection_type, :read_field, nil) }
-
- it 'does not fail when authorizing' do
- expect(resolved).to be_nil
- end
- end
-
- context 'when it returns values' do
- let(:objects) { [1, 2, 3] }
- let(:field_type) { type([:read_object]).connection_type }
- let(:type_class) { type_with_field(field_type, [], objects) }
-
- it 'filters out unauthorized values' do
- spy_ability_check_for(:read_object, 1, passed: true)
- spy_ability_check_for(:read_object, 2, passed: false)
- spy_ability_check_for(:read_object, 3, passed: true)
-
- expect(resolved.nodes).to eq [1, 3]
- end
- end
- end
-
- context 'when the field is a specific type' do
- let(:custom_type) { type(:read_type) }
- let(:object_in_field) { double('presented in field') }
-
- let(:type_class) { type_with_field(custom_type, :read_field, object_in_field) }
- let(:type_instance) { type_class.authorized_new(object_in_field, context) }
-
- it 'checks both field & type permissions' do
- spy_ability_check_for(:read_field, object_in_field, passed: true)
- spy_ability_check_for(:read_type, object_in_field, passed: true)
-
- expect(resolved).to eq(object_in_field)
- end
-
- it 'returns nil if viewing was not allowed' do
- spy_ability_check_for(:read_field, object_in_field, passed: false)
- spy_ability_check_for(:read_type, object_in_field, passed: true)
-
- expect(resolved).to be_nil
- end
-
- context 'when the field is not nullable' do
- let(:type_class) { type_with_field(custom_type, :read_field, object_in_field, null: false) }
-
- it 'returns nil when viewing is not allowed' do
- spy_ability_check_for(:read_type, object_in_field, passed: false)
-
- expect(resolved).to be_nil
- end
- end
-
- context 'when the field is a list' do
- let(:object_1) { double('presented in field 1') }
- let(:object_2) { double('presented in field 2') }
- let(:presented_types) { [double(object: object_1), double(object: object_2)] }
-
- let(:type_class) { type_with_field([custom_type], :read_field, presented_types) }
- let(:type_instance) { type_class.authorized_new(presented_types, context) }
-
- it 'checks all permissions' do
- allow(Ability).to receive(:allowed?) { true }
-
- spy_ability_check_for(:read_field, object_1, passed: true)
- spy_ability_check_for(:read_type, object_1, passed: true)
- spy_ability_check_for(:read_field, object_2, passed: true)
- spy_ability_check_for(:read_type, object_2, passed: true)
-
- expect(resolved).to eq(presented_types)
- end
-
- it 'filters out objects that the user cannot see' do
- allow(Ability).to receive(:allowed?) { true }
-
- spy_ability_check_for(:read_type, object_1, passed: false)
-
- expect(resolved).to contain_exactly(have_attributes(object: object_2))
- end
- end
- end
- end
-
- it_behaves_like 'authorizing fields'
- end
-
- private
-
- def spy_ability_check_for(ability, object, passed: true)
- expect(Ability)
- .to receive(:allowed?)
- .with(current_user, ability, object)
- .and_return(passed)
- end
-end
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index c5d7665c3b2..fb48d820057 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -22,6 +22,14 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
def current_user
user
end
+
+ def context
+ { current_user: user }
+ end
+
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(required_permissions)
+ end
end
end
@@ -30,9 +38,16 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
subject(:loading_resource) { fake_class.new(user, project) }
+ before do
+ # don't allow anything by default
+ allow(Ability).to receive(:allowed?) do
+ false
+ end
+ end
+
context 'when the user is allowed to perform the action' do
before do
- allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do
+ allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project) do
true
end
end
@@ -48,24 +63,12 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
expect { loading_resource.authorize!(project) }.not_to raise_error
end
end
-
- describe '#authorized_resource?' do
- it 'is true' do
- expect(loading_resource.authorized_resource?(project)).to be(true)
- end
- end
end
context 'when the user is not allowed to perform the action' do
- before do
- allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project, scope: :user) do
- false
- end
- end
-
describe '#authorized_find!' do
it 'raises an error' do
- expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect { loading_resource.authorized_find! }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
@@ -74,12 +77,6 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
-
- describe '#authorized_resource?' do
- it 'is false' do
- expect(loading_resource.authorized_resource?(project)).to be(false)
- end
- end
end
context 'when the class does not define #find_object' do
@@ -92,46 +89,6 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
- context 'when the class does not define authorize' do
- let(:fake_class) do
- Class.new do
- include Gitlab::Graphql::Authorize::AuthorizeResource
-
- attr_reader :user, :found_object
-
- def initialize(user, found_object)
- @user, @found_object = user, found_object
- end
-
- def find_object(*_args)
- found_object
- end
-
- def current_user
- user
- end
-
- def self.name
- 'TestClass'
- end
- end
- end
-
- let(:error) { /#{fake_class.name} has no authorizations/ }
-
- describe '#authorized_find!' do
- it 'raises a comprehensive error message' do
- expect { loading_resource.authorized_find! }.to raise_error(error)
- end
- end
-
- describe '#authorized_resource?' do
- it 'raises a comprehensive error message' do
- expect { loading_resource.authorized_resource?(project) }.to raise_error(error)
- end
- end
- end
-
describe '#authorize' do
it 'adds permissions from subclasses to those of superclasses when used on classes' do
base_class = Class.new do
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 9271b868e36..1a929373716 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -79,6 +79,21 @@ RSpec.describe Gitlab::Highlight do
expect(result).to eq(expected)
end
+
+ context 'when start line number is set' do
+ let(:expected) do
+ %q(<span id="LC10" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC11" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC12" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC13" class="line" lang="diff"> ddd</span>)
+ end
+
+ it 'highlights each line properly' do
+ result = described_class.new(file_name, content).highlight(content, context: { line_number: 10 })
+
+ expect(result).to eq(expected)
+ end
+ end
end
describe 'with CRLF' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index b1581bf02a6..5582ff25b04 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1358,7 +1358,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
let(:ineligible_total_categories) do
- %w[source_code ci_secrets_management incident_management_alerts snippets terraform epics_usage]
+ %w[source_code ci_secrets_management incident_management_alerts snippets terraform]
end
it 'has all known_events' do
diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb
new file mode 100644
index 00000000000..d45c8c2a8c5
--- /dev/null
+++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Rouge::Formatters::HTMLGitlab do
+ describe '#format' do
+ subject { described_class.format(tokens, options) }
+
+ let(:lang) { 'ruby' }
+ let(:lexer) { Rouge::Lexer.find_fancy(lang) }
+ let(:tokens) { lexer.lex("def hello", continue: false) }
+ let(:options) { { tag: lang } }
+
+ it 'returns highlighted ruby code' do
+ code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+
+ is_expected.to eq(code)
+ end
+
+ context 'when options are empty' do
+ let(:options) { {} }
+
+ it 'returns highlighted code without language' do
+ code = %q{<span id="LC1" class="line" lang=""><span class="k">def</span> <span class="nf">hello</span></span>}
+
+ is_expected.to eq(code)
+ end
+ end
+
+ context 'when line number is provided' do
+ let(:options) { { tag: lang, line_number: 10 } }
+
+ it 'returns highlighted ruby code with correct line number' do
+ code = %q{<span id="LC10" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
+
+ is_expected.to eq(code)
+ end
+ end
+ end
+end
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index 5e20eed877f..4c3e0228583 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
id: pipeline.id,
sha: pipeline.sha,
ref: pipeline.ref,
- git_commit_message: pipeline.git_commit_message,
user: user_info,
project: {
name: pipeline.project.name,
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index e24ab0b07f2..46ec22e7ef8 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe 'Reposition and move issue within board lists' do
let(:mutation_name) { mutation_class.graphql_name }
let(:mutation_result_identifier) { mutation_name.camelize(:lower) }
let(:current_user) { user }
- let(:params) { { board_id: board.to_global_id.to_s, project_path: project.full_path, iid: issue1.iid.to_s } }
+ let(:board_id) { global_id_of(board) }
+ let(:params) { { board_id: board_id, project_path: project.full_path, iid: issue1.iid.to_s } }
let(:issue_move_params) do
{
from_list_id: list1.id,
@@ -34,16 +35,44 @@ RSpec.describe 'Reposition and move issue within board lists' do
end
shared_examples 'returns an error' do
- it 'fails with error' do
- message = "The resource that you are attempting to access does not exist or you don't have "\
- "permission to perform this action"
+ let(:message) do
+ "The resource that you are attempting to access does not exist or you don't have " \
+ "permission to perform this action"
+ end
+ it 'fails with error' do
post_graphql_mutation(mutation(params), current_user: current_user)
expect(graphql_errors).to include(a_hash_including('message' => message))
end
end
+ context 'when the board_id is not a board' do
+ let(:board_id) { global_id_of(project) }
+ let(:issue_move_params) do
+ { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id }
+ end
+
+ it_behaves_like 'returns an error' do
+ let(:message) { include('does not represent an instance of') }
+ end
+ end
+
+ # This test aims to distinguish between the failures to authorize
+ # :read_issue_board and :update_issue
+ context 'when the user cannot read the issue board' do
+ let(:issue_move_params) do
+ { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id }
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).with(any_args).and_return(true)
+ allow(Ability).to receive(:allowed?).with(current_user, :read_issue_board, board).and_return(false)
+ end
+
+ it_behaves_like 'returns an error'
+ end
+
context 'when user has access to resources' do
context 'when repositioning an issue' do
let(:issue_move_params) { { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id } }
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100755..100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100755..100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore