diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-11-22 18:25:55 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-11-22 18:25:55 +0000 |
commit | 3c1f968369c5015810562c5cdbd977bdb5c7fa11 (patch) | |
tree | ac2e89b3f2411b7f9f48ee7b728067c4e56c0a4b /app | |
parent | afcfe182a64a9b793d81106da86688cc07a48fed (diff) | |
parent | 0c972f56b8193c173fffeccf1c9080c0682071e2 (diff) | |
download | gitlab-ce-3c1f968369c5015810562c5cdbd977bdb5c7fa11.tar.gz |
Merge branch 'master' into list-multiple-clusters
* master: (158 commits)
Improve output for extra queries in specs
Fixed new group milestone breadcrumb
Try to find the merge-base against the canonical master
Add FetchSourceBranch Gitaly call
Backport QA code that belongs to CE from EE Geo
Add QUERY_RECORDER_DEBUG environment variable to improve performance debugging
Add support of Mermaid
Update VERSION to 10.3.0-pre
Update CHANGELOG.md for 10.2.0
default fill color for SVGs
Fix reply quote keyboard shortcut on MRs
ignore hashed repos (for now) when using `rake gitlab:cleanup:repos`
Use Redis cache for branch existence checks
Update CONTRIBUTING.md: Link definition of done to criteria
Use `make install` for Gitaly setups in non-test environments
FileUploader should check for hashed_storage?(:attachments) to use disk_path
Set the default gitlab-shell timeout to 3 hours
Update composite pipelines index to include "id"
Use arrays in Pipeline#latest_builds_with_artifacts
Fix blank states using old css
...
Diffstat (limited to 'app')
160 files changed, 1486 insertions, 1106 deletions
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index b5500ac116f..6b06344f5ba 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -1,7 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ /* global EditBlob */ -/* global NewCommitForm */ - +import NewCommitForm from '../new_commit_form'; import EditBlob from './edit_blob'; import BlobFileDropzone from '../blob/blob_file_dropzone'; diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index ef4093b59e3..20d23162940 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,12 +1,13 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ -/* global BoardService */ import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; import Flash from '../flash'; +import { __ } from '../locale'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; +import sidebarEventHub from '../sidebar/event_hub'; import './models/issue'; import './models/label'; import './models/list'; @@ -14,7 +15,7 @@ import './models/milestone'; import './models/assignee'; import './stores/boards_store'; import './stores/modal_store'; -import './services/board_service'; +import BoardService from './services/board_service'; import './mixins/modal_mixins'; import './mixins/sortable_default_options'; import './filters/due_date_filters'; @@ -77,11 +78,16 @@ $(() => { }); Store.rootPath = this.boardsEndpoint; - // Listen for updateTokens event eventHub.$on('updateTokens', this.updateTokens); + eventHub.$on('newDetailIssue', this.updateDetailIssue); + eventHub.$on('clearDetailIssue', this.clearDetailIssue); + sidebarEventHub.$on('toggleSubscription', this.toggleSubscription); }, beforeDestroy() { eventHub.$off('updateTokens', this.updateTokens); + eventHub.$off('newDetailIssue', this.updateDetailIssue); + eventHub.$off('clearDetailIssue', this.clearDetailIssue); + sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, mounted () { this.filterManager = new FilteredSearchBoards(Store.filter, true); @@ -112,6 +118,46 @@ $(() => { methods: { updateTokens() { this.filterManager.updateTokens(); + }, + updateDetailIssue(newIssue) { + const sidebarInfoEndpoint = newIssue.sidebarInfoEndpoint; + if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { + newIssue.setFetchingState('subscriptions', true); + BoardService.getIssueInfo(sidebarInfoEndpoint) + .then(res => res.json()) + .then((data) => { + newIssue.setFetchingState('subscriptions', false); + newIssue.updateData({ + subscribed: data.subscribed, + }); + }) + .catch(() => { + newIssue.setFetchingState('subscriptions', false); + Flash(__('An error occurred while fetching sidebar data')); + }); + } + + Store.detail.issue = newIssue; + }, + clearDetailIssue() { + Store.detail.issue = {}; + }, + toggleSubscription(id) { + const issue = Store.detail.issue; + if (issue.id === id && issue.toggleSubscriptionEndpoint) { + issue.setFetchingState('subscriptions', true); + BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) + .then(() => { + issue.setFetchingState('subscriptions', false); + issue.updateData({ + subscribed: !issue.subscribed, + }); + }) + .catch(() => { + issue.setFetchingState('subscriptions', false); + Flash(__('An error occurred when toggling the notification subscription')); + }); + } } }, }); diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.vue index 079fb6438b9..0b220a56e0b 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,25 +1,11 @@ +<script> import './issue_card_inner'; +import eventHub from '../eventhub'; const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardsIssueCard', - template: ` - <li class="card" - :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" - :index="index" - :data-issue-id="issue.id" - @mousedown="mouseDown" - @mousemove="mouseMove" - @mouseup="showIssue($event)"> - <issue-card-inner - :list="list" - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :update-filters="true" /> - </li> - `, components: { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, @@ -56,12 +42,30 @@ export default { this.showDetail = false; if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { - Store.detail.issue = {}; + eventHub.$emit('clearDetailIssue'); } else { - Store.detail.issue = this.issue; + eventHub.$emit('newDetailIssue', this.issue); Store.detail.list = this.list; } } }, }, }; +</script> + +<template> + <li class="card" + :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + :index="index" + :data-issue-id="issue.id" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="showIssue($event)"> + <issue-card-inner + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :update-filters="true" /> + </li> +</template> diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 6159680f1e6..29aeb8e84aa 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,6 +1,6 @@ /* global Sortable */ import boardNewIssue from './board_new_issue'; -import boardCard from './board_card'; +import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 9ae5e270a4b..faa76da964f 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -5,12 +5,13 @@ import Vue from 'vue'; import Flash from '../../flash'; import eventHub from '../../sidebar/event_hub'; -import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; -import Assignees from '../../sidebar/components/assignees/assignees'; +import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; +import assignees from '../../sidebar/components/assignees/assignees'; import DueDateSelectors from '../../due_date_select'; import './sidebar/remove_issue'; import IssuableContext from '../../issuable_context'; import LabelsSelect from '../../labels_select'; +import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue'; const Store = gl.issueBoards.BoardsStore; @@ -117,11 +118,11 @@ gl.issueBoards.BoardSidebar = Vue.extend({ new DueDateSelectors(); new LabelsSelect(); new Sidebar(); - gl.Subscription.bindAll('.subscription'); }, components: { + assigneeTitle, + assignees, removeBtn: gl.issueBoards.RemoveIssueBtn, - 'assignee-title': AssigneeTitle, - assignees: Assignees, + subscriptions, }, }); diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 407db176446..10f85c1d676 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -17,6 +17,11 @@ class ListIssue { this.assignees = []; this.selected = false; this.position = obj.relative_position || Infinity; + this.isFetching = { + subscriptions: true, + }; + this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; + this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; if (obj.milestone) { this.milestone = new ListMilestone(obj.milestone); @@ -73,6 +78,14 @@ class ListIssue { return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); } + updateData(newData) { + Object.assign(this, newData); + } + + setFetchingState(key, value) { + this.isFetching[key] = value; + } + update (url) { const data = { issue: { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 97e80afa3f8..fa7ddd25e1f 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -2,7 +2,7 @@ import Vue from 'vue'; -class BoardService { +export default class BoardService { constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { issues: { @@ -88,6 +88,14 @@ class BoardService { return this.issues.bulkUpdate(data); } + + static getIssueInfo(endpoint) { + return Vue.http.get(endpoint); + } + + static toggleIssueSubscription(endpoint) { + return Vue.http.post(endpoint); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 0ac8e68187d..ce14c9a9945 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -1,10 +1,7 @@ -import axios from 'axios'; -import setAxiosCsrfToken from '../../lib/utils/axios_utils'; +import axios from '../../lib/utils/axios_utils'; export default class ClusterService { constructor(options = {}) { - setAxiosCsrfToken(); - this.options = options; this.appInstallEndpointMap = { helm: this.options.installHelmEndpoint, @@ -18,7 +15,6 @@ export default class ClusterService { } installApplication(appId) { - const endpoint = this.appInstallEndpointMap[appId]; - return axios.post(endpoint); + return axios.post(this.appInstallEndpointMap[appId]); } } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d716218d9a4..34708977d20 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,12 +1,12 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ import { s__ } from './locale'; -/* global ProjectSelect */ +import projectSelect from './project_select'; import IssuableIndex from './issuable_index'; -/* global Milestone */ +import Milestone from './milestone'; import IssuableForm from './issuable_form'; import LabelsSelect from './labels_select'; /* global MilestoneSelect */ -/* global NewBranchForm */ +import NewBranchForm from './new_branch_form'; /* global NotificationsForm */ /* global NotificationsDropdown */ import groupAvatar from './group_avatar'; @@ -18,16 +18,14 @@ import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ import NamespaceSelect from './namespace_select'; -/* global NewCommitForm */ -/* global NewBranchForm */ +import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; /* global MergeRequest */ /* global Compare */ /* global CompareAutocomplete */ /* global ProjectFindFile */ -/* global ProjectNew */ -/* global ProjectShow */ +import ProjectNew from './project_new'; import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; @@ -91,6 +89,8 @@ import Members from './members'; import memberExpirationDate from './member_expiration_date'; import DueDateSelectors from './due_date_select'; import Diff from './diff'; +import ProjectLabelSubscription from './project_label_subscription'; +import ProjectVariables from './project_variables'; (function() { var Dispatcher; @@ -187,7 +187,7 @@ import Diff from './diff'; initIssuableSidebar(); break; case 'dashboard:milestones:index': - new ProjectSelect(); + projectSelect(); break; case 'projects:milestones:show': case 'groups:milestones:show': @@ -197,7 +197,7 @@ import Diff from './diff'; break; case 'dashboard:issues': case 'dashboard:merge_requests': - new ProjectSelect(); + projectSelect(); initLegacyFilters(); break; case 'groups:issues': @@ -206,7 +206,7 @@ import Diff from './diff'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } - new ProjectSelect(); + projectSelect(); break; case 'dashboard:todos:index': new Todos(); @@ -317,7 +317,6 @@ import Diff from './diff'; break; case 'projects:merge_requests:show': new Diff(); - shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); initIssuableSidebar(); @@ -327,6 +326,8 @@ import Diff from './diff'; window.mergeRequest = new MergeRequest({ action: mrShowNode.dataset.mrAction, }); + + shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': new gl.Activities(); @@ -339,7 +340,8 @@ import Diff from './diff'; container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); - initChangesDropdown(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': @@ -484,7 +486,7 @@ import Diff from './diff'; if ($el.find('.dropdown-group-label').length) { new GroupLabelSubscription($el); } else { - new gl.ProjectLabelSubscription($el); + new ProjectLabelSubscription($el); } }); break; @@ -520,7 +522,7 @@ import Diff from './diff'; // Initialize expandable settings panels initSettingsPanels(); case 'groups:settings:ci_cd:show': - new gl.ProjectVariables(); + new ProjectVariables(); break; case 'ci:lints:create': case 'ci:lints:show': @@ -623,7 +625,6 @@ import Diff from './diff'; case 'show': new Star(); new ProjectNew(); - new ProjectShow(); new NotificationsDropdown(); break; case 'wikis': diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index c039ae85cfb..ffb7757bed8 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -227,25 +227,27 @@ export default { /> <div - class="blank-state blank-state-no-icon" + class="blank-state-row" v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments + <div class="blank-state-center"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments + </a> + </p> + + <a + v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New environment </a> - </p> - - <a - v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New environment - </a> + </div> </div> <div diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 1191e0b895e..ada693afc46 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -14,7 +14,6 @@ export default () => { }); new LabelsSelect(); new IssuableContext(sidebarOptions.currentUser); - gl.Subscription.bindAll('.subscription'); new DueDateSelectors(); window.sidebar = new Sidebar(); }; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js index 1b265721581..2cbb70220d0 100644 --- a/app/assets/javascripts/init_legacy_filters.js +++ b/app/assets/javascripts/init_legacy_filters.js @@ -1,8 +1,7 @@ /* eslint-disable no-new */ import LabelsSelect from './labels_select'; /* global MilestoneSelect */ -/* global SubscriptionSelect */ - +import subscriptionSelect from './subscription_select'; import UsersSelect from './users_select'; import issueStatusSelect from './issue_status_select'; @@ -11,5 +10,5 @@ export default () => { new LabelsSelect(); new MilestoneSelect(); issueStatusSelect(); - new SubscriptionSelect(); + subscriptionSelect(); }; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index af6358953cf..ba2b6737988 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -1,11 +1,10 @@ /* eslint-disable class-methods-use-this, no-new */ /* global MilestoneSelect */ -/* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import './milestone_select'; import issueStatusSelect from './issue_status_select'; -import './subscription_select'; +import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; const HIDDEN_CLASS = 'hidden'; @@ -48,7 +47,7 @@ export default class IssuableBulkUpdateSidebar { new LabelsSelect(); new MilestoneSelect(); issueStatusSelect(); - new SubscriptionSelect(); + subscriptionSelect(); } setupBulkUpdateActions() { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e8ac8d3b5bb..4e39d483b31 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -102,6 +102,11 @@ export default { required: false, default: 'issue', }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, data() { const store = new Store({ @@ -234,6 +239,7 @@ export default { :project-path="projectPath" :project-namespace="projectNamespace" :show-delete-button="showDeleteButton" + :can-attach-file="canAttachFile" /> <div v-else> <title-component diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 0aa1b2c2e31..4d2ef409bad 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -17,6 +17,11 @@ type: String, required: true, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, components: { markdownField, @@ -36,7 +41,8 @@ </label> <markdown-field :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath"> + :markdown-docs-path="markdownDocsPath" + :can-attach-file="canAttachFile"> <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 8bb5c86d567..d61776d480d 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -41,6 +41,11 @@ required: false, default: true, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, components: { lockedWarning, @@ -83,7 +88,8 @@ <description-field :form-state="formState" :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" /> + :markdown-docs-path="markdownDocsPath" + :can-attach-file="canAttachFile" /> <edit-actions :form-state="formState" :can-destroy="canDestroy" diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index 3e2658f9fc1..5a216f8fae2 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -29,8 +29,8 @@ export default class JobMediator { this.poll = new Poll({ resource: this.service, method: 'getJob', - successCallback: this.successCallback.bind(this), - errorCallback: this.errorCallback.bind(this), + successCallback: response => this.successCallback(response), + errorCallback: () => this.errorCallback(), }); if (!Visibility.hidden()) { @@ -57,7 +57,7 @@ export default class JobMediator { successCallback(response) { this.state.isLoading = false; - return response.json().then(data => this.store.storeJob(data)); + return this.store.storeJob(response.data); } errorCallback() { diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js index eaf1c6e500a..b746489c45c 100644 --- a/app/assets/javascripts/jobs/services/job_service.js +++ b/app/assets/javascripts/jobs/services/job_service.js @@ -1,14 +1,11 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '../../lib/utils/axios_utils'; export default class JobService { constructor(endpoint) { - this.job = Vue.resource(endpoint); + this.job = endpoint; } getJob() { - return this.job.get(); + return axios.get(this.job); } } diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 45bff245827..7aeeca3b283 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -1,6 +1,22 @@ import axios from 'axios'; import csrf from './csrf'; -export default function setAxiosCsrfToken() { - axios.defaults.headers.common[csrf.headerKey] = csrf.token; -} +axios.defaults.headers.common[csrf.headerKey] = csrf.token; + +// Maintain a global counter for active requests +// see: spec/support/wait_for_requests.rb +axios.interceptors.request.use((config) => { + window.activeVueResources = window.activeVueResources || 0; + window.activeVueResources += 1; + + return config; +}); + +// Remove the global counter +axios.interceptors.response.use((config) => { + window.activeVueResources -= 1; + + return config; +}); + +export default axios; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 65a8cf2c891..7fca80c2fdb 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils'; /** * Polling utility for handling realtime updates. - * Service for vue resouce and method need to be provided as props + * Requirements: Promise based HTTP client + * + * Service for promise based http client and method need to be provided as props * * @example * new Poll({ diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0035dd23011..d908452399c 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -29,7 +29,6 @@ import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; -import './lib/utils/pretty_time'; import './lib/utils/url_utility'; // behaviors @@ -59,11 +58,7 @@ import './line_highlighter'; import initLogoAnimation from './logo'; import './merge_request'; import './merge_request_tabs'; -import './milestone'; import './milestone_select'; -import './namespace_select'; -import './new_branch_form'; -import './new_commit_form'; import './notes'; import './notifications_dropdown'; import './notifications_form'; @@ -71,22 +66,15 @@ import './pager'; import './preview_markdown'; import './project_find_file'; import './project_import'; -import './project_label_subscription'; -import './project_new'; -import './project_select'; -import './project_show'; -import './project_variables'; import './projects_dropdown'; import './projects_list'; import './syntax_highlight'; import './render_math'; +import './render_mermaid'; import './render_gfm'; import './right_sidebar'; import './search'; import './search_autocomplete'; -import './smart_interval'; -import './subscription'; -import './subscription_select'; import initBreadcrumbs from './breadcrumb'; import './dispatcher'; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 8f3f1986763..f76a998bf8c 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,54 +1,49 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ /* global Sortable */ import Flash from './flash'; -(function() { - this.Milestone = (function() { - function Milestone() { - this.bindTabsSwitching(); +export default class Milestone { + constructor() { + this.bindTabsSwitching(); - // Load merge request tab if it is active - // merge request tab is active based on different conditions in the backend - this.loadTab($('.js-milestone-tabs .active a')); + // Load merge request tab if it is active + // merge request tab is active based on different conditions in the backend + this.loadTab($('.js-milestone-tabs .active a')); - this.loadInitialTab(); - } + this.loadInitialTab(); + } + + bindTabsSwitching() { + return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { + const $target = $(e.target); - Milestone.prototype.bindTabsSwitching = function() { - return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { - const $target = $(e.target); + location.hash = $target.attr('href'); + this.loadTab($target); + }); + } + // eslint-disable-next-line class-methods-use-this + loadInitialTab() { + const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); - location.hash = $target.attr('href'); - this.loadTab($target); + if ($target.length) { + $target.tab('show'); + } + } + // eslint-disable-next-line class-methods-use-this + loadTab($target) { + const endpoint = $target.data('endpoint'); + const tabElId = $target.attr('href'); + + if (endpoint && !$target.hasClass('is-loaded')) { + $.ajax({ + url: endpoint, + dataType: 'JSON', + }) + .fail(() => new Flash('Error loading milestone tab')) + .done((data) => { + $(tabElId).html(data.html); + $target.addClass('is-loaded'); }); - }; - - Milestone.prototype.loadInitialTab = function() { - const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); - - if ($target.length) { - $target.tab('show'); - } - }; - - Milestone.prototype.loadTab = function($target) { - const endpoint = $target.data('endpoint'); - const tabElId = $target.attr('href'); - - if (endpoint && !$target.hasClass('is-loaded')) { - $.ajax({ - url: endpoint, - dataType: 'JSON', - }) - .fail(() => new Flash('Error loading milestone tab')) - .done((data) => { - $(tabElId).html(data.html); - $target.addClass('is-loaded'); - }); - } - }; - - return Milestone; - })(); -}).call(window); + } + } +} diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 39fb302b644..77733b67c4d 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,97 +1,93 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ -import RefSelectDropdown from '~/ref_select_dropdown'; +import RefSelectDropdown from './ref_select_dropdown'; -(function() { - this.NewBranchForm = (function() { - function NewBranchForm(form, availableRefs) { - this.validate = this.validate.bind(this); - this.branchNameError = form.find('.js-branch-name-error'); - this.name = form.find('.js-branch-name'); - this.ref = form.find('#ref'); - new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new - this.setupRestrictions(); - this.addBinding(); - this.init(); +export default class NewBranchForm { + constructor(form, availableRefs) { + this.validate = this.validate.bind(this); + this.branchNameError = form.find('.js-branch-name-error'); + this.name = form.find('.js-branch-name'); + this.ref = form.find('#ref'); + new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new + this.setupRestrictions(); + this.addBinding(); + this.init(); + } + + addBinding() { + return this.name.on('blur', this.validate); + } + + init() { + if (this.name.length && this.name.val().length > 0) { + return this.name.trigger('blur'); } + } - NewBranchForm.prototype.addBinding = function() { - return this.name.on('blur', this.validate); + setupRestrictions() { + var endsWith, invalid, single, startsWith; + startsWith = { + pattern: /^(\/|\.)/g, + prefix: "can't start with", + conjunction: "or" }; - - NewBranchForm.prototype.init = function() { - if (this.name.length && this.name.val().length > 0) { - return this.name.trigger('blur'); - } + endsWith = { + pattern: /(\/|\.|\.lock)$/g, + prefix: "can't end in", + conjunction: "or" }; - - NewBranchForm.prototype.setupRestrictions = function() { - var endsWith, invalid, single, startsWith; - startsWith = { - pattern: /^(\/|\.)/g, - prefix: "can't start with", - conjunction: "or" - }; - endsWith = { - pattern: /(\/|\.|\.lock)$/g, - prefix: "can't end in", - conjunction: "or" - }; - invalid = { - pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, - prefix: "can't contain", - conjunction: ", " - }; - single = { - pattern: /^@+$/g, - prefix: "can't be", - conjunction: "or" - }; - return this.restrictions = [startsWith, invalid, endsWith, single]; + invalid = { + pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, + prefix: "can't contain", + conjunction: ", " + }; + single = { + pattern: /^@+$/g, + prefix: "can't be", + conjunction: "or" }; + return this.restrictions = [startsWith, invalid, endsWith, single]; + } - NewBranchForm.prototype.validate = function() { - var errorMessage, errors, formatter, unique, validator; - const indexOf = [].indexOf; + validate() { + var errorMessage, errors, formatter, unique, validator; + const indexOf = [].indexOf; - this.branchNameError.empty(); - unique = function(values, value) { - if (indexOf.call(values, value) === -1) { - values.push(value); - } - return values; - }; - formatter = function(values, restriction) { - var formatted; - formatted = values.map(function(value) { - switch (false) { - case !/\s/.test(value): - return 'spaces'; - case !/\/{2,}/g.test(value): - return 'consecutive slashes'; - default: - return "'" + value + "'"; - } - }); - return restriction.prefix + " " + (formatted.join(restriction.conjunction)); - }; - validator = (function(_this) { - return function(errors, restriction) { - var matched; - matched = _this.name.val().match(restriction.pattern); - if (matched) { - return errors.concat(formatter(matched.reduce(unique, []), restriction)); - } else { - return errors; - } - }; - })(this); - errors = this.restrictions.reduce(validator, []); - if (errors.length > 0) { - errorMessage = $("<span/>").text(errors.join(', ')); - return this.branchNameError.append(errorMessage); + this.branchNameError.empty(); + unique = function(values, value) { + if (indexOf.call(values, value) === -1) { + values.push(value); } + return values; }; - - return NewBranchForm; - })(); -}).call(window); + formatter = function(values, restriction) { + var formatted; + formatted = values.map(function(value) { + switch (false) { + case !/\s/.test(value): + return 'spaces'; + case !/\/{2,}/g.test(value): + return 'consecutive slashes'; + default: + return "'" + value + "'"; + } + }); + return restriction.prefix + " " + (formatted.join(restriction.conjunction)); + }; + validator = (function(_this) { + return function(errors, restriction) { + var matched; + matched = _this.name.val().match(restriction.pattern); + if (matched) { + return errors.concat(formatter(matched.reduce(unique, []), restriction)); + } else { + return errors; + } + }; + })(this); + errors = this.restrictions.reduce(validator, []); + if (errors.length > 0) { + errorMessage = $("<span/>").text(errors.join(', ')); + return this.branchNameError.append(errorMessage); + } + } +} diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 04073ef7270..6e152497d20 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,32 +1,28 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */ -(function() { - this.NewCommitForm = (function() { - function NewCommitForm(form) { - this.form = form; - this.renderDestination = this.renderDestination.bind(this); - this.branchName = form.find('.js-branch-name'); - this.originalBranch = form.find('.js-original-branch'); - this.createMergeRequest = form.find('.js-create-merge-request'); - this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); - this.branchName.keyup(this.renderDestination); - this.renderDestination(); - } +export default class NewCommitForm { + constructor(form) { + this.form = form; + this.renderDestination = this.renderDestination.bind(this); + this.branchName = form.find('.js-branch-name'); + this.originalBranch = form.find('.js-original-branch'); + this.createMergeRequest = form.find('.js-create-merge-request'); + this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); + this.branchName.keyup(this.renderDestination); + this.renderDestination(); + } - NewCommitForm.prototype.renderDestination = function() { - var different; - different = this.branchName.val() !== this.originalBranch.val(); - if (different) { - this.createMergeRequestContainer.show(); - if (!this.wasDifferent) { - this.createMergeRequest.prop('checked', true); - } - } else { - this.createMergeRequestContainer.hide(); - this.createMergeRequest.prop('checked', false); + renderDestination() { + var different; + different = this.branchName.val() !== this.originalBranch.val(); + if (different) { + this.createMergeRequestContainer.show(); + if (!this.wasDifferent) { + this.createMergeRequest.prop('checked', true); } - return this.wasDifferent = different; - }; - - return NewCommitForm; - })(); -}).call(window); + } else { + this.createMergeRequestContainer.hide(); + this.createMergeRequest.prop('checked', false); + } + return this.wasDifferent = different; + } +} diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue index e73ec2aaf71..64466b04b40 100644 --- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue @@ -1,18 +1,21 @@ <script> + import Icon from '../../vue_shared/components/icon.vue'; + export default { - computed: { - lockIcon() { - return gl.utils.spriteIcon('lock'); - }, + component: { + Icon, }, }; - </script> <template> <div class="disabled-comment text-center"> - <span class="issuable-note-warning"> - <span class="icon" v-html="lockIcon"></span> + <span class="issuable-note-warning inline"> + <icon + name="lock" + :size="16" + class="icon"> + </icon> <span>This issue is locked. Only <b>project members</b> can comment.</span> </span> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index cf241c8ffed..233be8a49c8 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -267,9 +267,11 @@ /> <div - class="blank-state blank-state-no-icon" + class="blank-state-row" v-if="shouldRenderNoPipelinesMessage"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + <div class="blank-state-center"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> </div> <div diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index ddb78aaeea1..36b6a5ed376 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global ProjectSelect */ import Cookies from 'js-cookie'; +import projectSelect from './project_select'; export default class Project { constructor() { @@ -46,7 +46,7 @@ export default class Project { } static projectSelectDropdown () { - new ProjectSelect(); + projectSelect(); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 0a811627600..b65521b278f 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,55 +1,50 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ +export default class ProjectLabelSubscription { + constructor(container) { + this.$container = $(container); + this.$buttons = this.$container.find('.js-subscribe-button'); -(function(global) { - class ProjectLabelSubscription { - constructor(container) { - this.$container = $(container); - this.$buttons = this.$container.find('.js-subscribe-button'); - - this.$buttons.on('click', this.toggleSubscription.bind(this)); - } + this.$buttons.on('click', this.toggleSubscription.bind(this)); + } - toggleSubscription(event) { - event.preventDefault(); + toggleSubscription(event) { + event.preventDefault(); - const $btn = $(event.currentTarget); - const $span = $btn.find('span'); - const url = $btn.attr('data-url'); - const oldStatus = $btn.attr('data-status'); + const $btn = $(event.currentTarget); + const $span = $btn.find('span'); + const url = $btn.attr('data-url'); + const oldStatus = $btn.attr('data-status'); - $btn.addClass('disabled'); - $span.toggleClass('hidden'); + $btn.addClass('disabled'); + $span.toggleClass('hidden'); - $.ajax({ - type: 'POST', - url: url - }).done(() => { - let newStatus, newAction; + $.ajax({ + type: 'POST', + url, + }).done(() => { + let newStatus; + let newAction; - if (oldStatus === 'unsubscribed') { - [newStatus, newAction] = ['subscribed', 'Unsubscribe']; - } else { - [newStatus, newAction] = ['unsubscribed', 'Subscribe']; - } + if (oldStatus === 'unsubscribed') { + [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + } else { + [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + } - $span.toggleClass('hidden'); - $btn.removeClass('disabled'); + $span.toggleClass('hidden'); + $btn.removeClass('disabled'); - this.$buttons.attr('data-status', newStatus); - this.$buttons.find('> span').text(newAction); + this.$buttons.attr('data-status', newStatus); + this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { - const $button = $(button); + this.$buttons.map((button) => { + const $button = $(button); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); - } + if ($button.attr('data-original-title')) { + $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); + } - return button; - }); + return button; }); - } + }); } - - global.ProjectLabelSubscription = ProjectLabelSubscription; -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index fd89a1a85c3..ca548d011b6 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ import VisibilitySelect from './visibility_select'; @@ -7,153 +7,145 @@ function highlightChanges($elm) { setTimeout(() => $elm.removeClass('highlight-changes'), 10); } -(function() { - this.ProjectNew = (function() { - function ProjectNew() { - this.toggleSettings = this.toggleSettings.bind(this); - this.$selects = $('.features select'); - this.$repoSelects = this.$selects.filter('.js-repo-select'); - this.$projectSelects = this.$selects.not('.js-repo-select'); - - $('.project-edit-container').on('ajax:before', (function(_this) { - return function() { - $('.project-edit-container').hide(); - return $('.save-project-loader').show(); - }; - })(this)); - - this.initVisibilitySelect(); - - this.toggleSettings(); - this.toggleSettingsOnclick(); - this.toggleRepoVisibility(); - } - - ProjectNew.prototype.initVisibilitySelect = function() { - const visibilityContainer = document.querySelector('.js-visibility-select'); - if (!visibilityContainer) return; - const visibilitySelect = new VisibilitySelect(visibilityContainer); - visibilitySelect.init(); - - const $visibilitySelect = $(visibilityContainer).find('select'); - let projectVisibility = $visibilitySelect.val(); - const PROJECT_VISIBILITY_PRIVATE = '0'; - - $visibilitySelect.on('change', () => { - const newProjectVisibility = $visibilitySelect.val(); - - if (projectVisibility !== newProjectVisibility) { - this.$projectSelects.each((idx, select) => { - const $select = $(select); - const $options = $select.find('option'); - const values = $.map($options, e => e.value); - - // if switched to "private", limit visibility options - if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { - if ($select.val() !== values[0] && $select.val() !== values[1]) { - $select.val(values[1]).trigger('change'); - highlightChanges($select); - } - $options.slice(2).disable(); +export default class ProjectNew { + constructor() { + this.toggleSettings = this.toggleSettings.bind(this); + this.$selects = $('.features select'); + this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); + + $('.project-edit-container').on('ajax:before', () => { + $('.project-edit-container').hide(); + return $('.save-project-loader').show(); + }); + + this.initVisibilitySelect(); + + this.toggleSettings(); + this.toggleSettingsOnclick(); + this.toggleRepoVisibility(); + } + + initVisibilitySelect() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); } + $options.slice(2).disable(); + } - // if switched from "private", increase visibility for non-disabled options - if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { - $options.enable(); - if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { - $select.val(values[values.length - 1]).trigger('change'); - highlightChanges($select); - } + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); } - }); + } + }); - projectVisibility = newProjectVisibility; - } - }); - }; - - ProjectNew.prototype.toggleSettings = function() { - var self = this; - - this.$selects.each(function () { - var $select = $(this); - var className = $select.data('field') - .replace(/_/g, '-') - .replace('access-level', 'feature'); - self._showOrHide($select, '.' + className); - }); - }; - - ProjectNew.prototype.toggleSettingsOnclick = function() { - this.$selects.on('change', this.toggleSettings); - }; - - ProjectNew.prototype._showOrHide = function(checkElement, container) { - var $container = $(container); - - if ($(checkElement).val() !== '0') { - return $container.show(); - } else { - return $container.hide(); + projectVisibility = newProjectVisibility; } - }; - - ProjectNew.prototype.toggleRepoVisibility = function () { - var $repoAccessLevel = $('.js-repo-access-level select'); - var $lfsEnabledOption = $('.js-lfs-enabled select'); - var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; - var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); - var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") - .nextAll() - .hide(); - - $repoAccessLevel.off('change') - .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.each(function () { - var $this = $(this); - var repoSelectVal = parseInt($this.val(), 10); - - $this.find('option').enable(); - - if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { - $this.val(selectedVal).trigger('change'); - highlightChanges($this); - } - - $this.find("option[value='" + selectedVal + "']").nextAll().disable(); - }); + }); + } + + toggleSettings() { + this.$selects.each(function () { + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); + ProjectNew._showOrHide($select, '.' + className); + }); + } + + toggleSettingsOnclick() { + this.$selects.on('change', this.toggleSettings); + } + + static _showOrHide(checkElement, container) { + const $container = $(container); + + if ($(checkElement).val() !== '0') { + return $container.show(); + } + return $container.hide(); + } + + toggleRepoVisibility() { + var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") + .nextAll() + .hide(); + + $repoAccessLevel + .off('change') + .on('change', function () { + var selectedVal = parseInt($repoAccessLevel.val(), 10); + + this.$repoSelects.each(function () { + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); + + $this.find('option').enable(); + + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); + } - if (selectedVal) { - this.$repoSelects.removeClass('disabled'); + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); + }); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.removeClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = ''; - } - } else { - this.$repoSelects.addClass('disabled'); + if (selectedVal) { + this.$repoSelects.removeClass('disabled'); - if ($lfsEnabledOption.length) { - $lfsEnabledOption.val('false').addClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = 'none'; - containerRegistryCheckbox.checked = false; - } + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = ''; } + } else { + this.$repoSelects.addClass('disabled'); - prevSelectedVal = selectedVal; - }.bind(this)); - }; + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } + if (containerRegistry) { + containerRegistry.style.display = 'none'; + containerRegistryCheckbox.checked = false; + } + } - return ProjectNew; - })(); -}).call(window); + prevSelectedVal = selectedVal; + }.bind(this)); + } +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bffc85e6315..07a49d1506c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -2,79 +2,73 @@ import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; -(function () { - this.ProjectSelect = (function () { - function ProjectSelect() { - $('.ajax-project-select').each(function(i, select) { - var placeholder; - const simpleFilter = $(select).data('simple-filter') || false; - this.groupId = $(select).data('group-id'); - this.includeGroups = $(select).data('include-groups'); - this.allProjects = $(select).data('all-projects') || false; - this.orderBy = $(select).data('order-by') || 'id'; - this.withIssuesEnabled = $(select).data('with-issues-enabled'); - this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); +export default function projectSelect() { + $('.ajax-project-select').each(function(i, select) { + var placeholder; + const simpleFilter = $(select).data('simple-filter') || false; + this.groupId = $(select).data('group-id'); + this.includeGroups = $(select).data('include-groups'); + this.allProjects = $(select).data('all-projects') || false; + this.orderBy = $(select).data('order-by') || 'id'; + this.withIssuesEnabled = $(select).data('with-issues-enabled'); + this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled'); - placeholder = "Search for project"; - if (this.includeGroups) { - placeholder += " or group"; - } + placeholder = "Search for project"; + if (this.includeGroups) { + placeholder += " or group"; + } - $(select).select2({ - placeholder: placeholder, - minimumInputLength: 0, - query: (function (_this) { - return function (query) { - var finalCallback, projectsCallback; - finalCallback = function (projects) { + $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function (_this) { + return function (query) { + var finalCallback, projectsCallback; + finalCallback = function (projects) { + var data; + data = { + results: projects + }; + return query.callback(data); + }; + if (_this.includeGroups) { + projectsCallback = function (projects) { + var groupsCallback; + groupsCallback = function (groups) { var data; - data = { - results: projects - }; - return query.callback(data); + data = groups.concat(projects); + return finalCallback(data); }; - if (_this.includeGroups) { - projectsCallback = function (projects) { - var groupsCallback; - groupsCallback = function (groups) { - var data; - data = groups.concat(projects); - return finalCallback(data); - }; - return Api.groups(query.term, {}, groupsCallback); - }; - } else { - projectsCallback = finalCallback; - } - if (_this.groupId) { - return Api.groupProjects(_this.groupId, query.term, projectsCallback); - } else { - return Api.projects(query.term, { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, projectsCallback); - } + return Api.groups(query.term, {}, groupsCallback); }; - })(this), - id: function(project) { - if (simpleFilter) return project.id; - return JSON.stringify({ - name: project.name, - url: project.web_url, - }); - }, - text: function (project) { - return project.name_with_namespace || project.name; - }, - dropdownCssClass: "ajax-project-dropdown" + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects(_this.groupId, query.term, projectsCallback); + } else { + return Api.projects(query.term, { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + membership: !_this.allProjects, + }, projectsCallback); + } + }; + })(this), + id: function(project) { + if (simpleFilter) return project.id; + return JSON.stringify({ + name: project.name, + url: project.web_url, }); - if (simpleFilter) return select; - return new ProjectSelectComboButton(select); - }); - } - - return ProjectSelect; - })(); -}).call(window); + }, + text: function (project) { + return project.name_with_namespace || project.name; + }, + dropdownCssClass: "ajax-project-dropdown" + }); + if (simpleFilter) return select; + return new ProjectSelectComboButton(select); + }); +} diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js deleted file mode 100644 index 3a51c1f26ac..00000000000 --- a/app/assets/javascripts/project_show.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ - -(function() { - this.ProjectShow = (function() { - function ProjectShow() {} - - return ProjectShow; - })(); -}).call(window); - -// I kept class for future diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js index 4ee2e49306d..567c311f119 100644 --- a/app/assets/javascripts/project_variables.js +++ b/app/assets/javascripts/project_variables.js @@ -1,43 +1,39 @@ -(() => { - const HIDDEN_VALUE_TEXT = '******'; - class ProjectVariables { - constructor() { - this.$revealBtn = $('.js-btn-toggle-reveal-values'); - this.$revealBtn.on('click', this.toggleRevealState.bind(this)); - } +const HIDDEN_VALUE_TEXT = '******'; + +export default class ProjectVariables { + constructor() { + this.$revealBtn = $('.js-btn-toggle-reveal-values'); + this.$revealBtn.on('click', this.toggleRevealState.bind(this)); + } - toggleRevealState(e) { - e.preventDefault(); + toggleRevealState(e) { + e.preventDefault(); - const oldStatus = this.$revealBtn.attr('data-status'); - let newStatus = 'hidden'; - let newAction = 'Reveal Values'; + const oldStatus = this.$revealBtn.attr('data-status'); + let newStatus = 'hidden'; + let newAction = 'Reveal Values'; - if (oldStatus === 'hidden') { - newStatus = 'revealed'; - newAction = 'Hide Values'; - } + if (oldStatus === 'hidden') { + newStatus = 'revealed'; + newAction = 'Hide Values'; + } - this.$revealBtn.attr('data-status', newStatus); + this.$revealBtn.attr('data-status', newStatus); - const $variables = $('.variable-value'); + const $variables = $('.variable-value'); - $variables.each((_, variable) => { - const $variable = $(variable); - let newText = HIDDEN_VALUE_TEXT; + $variables.each((_, variable) => { + const $variable = $(variable); + let newText = HIDDEN_VALUE_TEXT; - if (newStatus === 'revealed') { - newText = $variable.attr('data-value'); - } + if (newStatus === 'revealed') { + newText = $variable.attr('data-value'); + } - $variable.text(newText); - }); + $variable.text(newText); + }); - this.$revealBtn.text(newAction); - } + this.$revealBtn.text(newAction); } - - window.gl = window.gl || {}; - window.gl.ProjectVariables = ProjectVariables; -})(); +} diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index bcdc0fd67b8..bf6fc0ec305 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -2,12 +2,13 @@ // Render Gitlab flavoured Markdown // -// Delegates to syntax highlight and render math +// Delegates to syntax highlight and render math & mermaid diagrams. // (function() { $.fn.renderGFM = function() { this.find('.js-syntax-highlight').syntaxHighlight(); this.find('.js-render-math').renderMath(); + this.find('.js-render-mermaid').renderMermaid(); return this; }; diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js new file mode 100644 index 00000000000..a253601f8e8 --- /dev/null +++ b/app/assets/javascripts/render_mermaid.js @@ -0,0 +1,30 @@ +// Renders diagrams and flowcharts from text using Mermaid in any element with the +// `js-render-mermaid` class. +// +// Example markup: +// +// <pre class="js-render-mermaid"> +// graph TD; +// A-- > B; +// A-- > C; +// B-- > D; +// C-- > D; +// </pre> +// + +import Flash from './flash'; + +$.fn.renderMermaid = function renderMermaid() { + if (this.length === 0) return; + + import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { + mermaid.initialize({ + loadOnStart: false, + theme: 'neutral', + }); + + mermaid.init(undefined, this); + }).catch((err) => { + Flash(`Can't load mermaid module: ${err}`); + }); +}; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 22a9a34dda3..6ee4d487c0b 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,10 +1,12 @@ <script> import Flash from '../../../flash'; import editForm from './edit_form.vue'; +import Icon from '../../../vue_shared/components/icon.vue'; export default { components: { editForm, + Icon, }, props: { isConfidential: { @@ -26,11 +28,8 @@ export default { }; }, computed: { - faEye() { - const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye'; - return { - [eye]: true, - }; + confidentialityIcon() { + return this.isConfidential ? 'eye-slash' : 'eye'; }, }, methods: { @@ -49,7 +48,11 @@ export default { <template> <div class="block issuable-sidebar-item confidentiality"> <div class="sidebar-collapsed-icon"> - <i class="fa" :class="faEye" aria-hidden="true"></i> + <icon + :name="confidentialityIcon" + :size="16" + aria-hidden="true"> + </icon> </div> <div class="title hide-collapsed"> Confidentiality @@ -70,11 +73,21 @@ export default { :update-confidential-attribute="updateConfidentialAttribute" /> <div v-if="!isConfidential" class="no-value sidebar-item-value"> - <i class="fa fa-eye sidebar-item-icon"></i> + <icon + name="eye" + :size="16" + aria-hidden="true" + class="sidebar-item-icon inline"> + </icon> Not confidential </div> <div v-else class="value sidebar-item-value hide-collapsed"> - <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i> + <icon + name="eye-slash" + :size="16" + aria-hidden="true" + class="sidebar-item-icon inline is-active"> + </icon> This issue is confidential </div> </div> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index c4b2900e020..9aff53cf8af 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -2,6 +2,7 @@ /* global Flash */ import editForm from './edit_form.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; +import Icon from '../../../vue_shared/components/icon.vue'; export default { props: { @@ -35,11 +36,12 @@ export default { components: { editForm, + Icon, }, computed: { - lockIconClass() { - return this.isLocked ? 'fa-lock' : 'fa-unlock'; + lockIcon() { + return this.isLocked ? 'lock' : 'lock-open'; }, isLockDialogOpen() { @@ -66,11 +68,12 @@ export default { <template> <div class="block issuable-sidebar-item lock"> <div class="sidebar-collapsed-icon"> - <i - class="fa" - :class="lockIconClass" + <icon + :name="lockIcon" + :size="16" aria-hidden="true" - ></i> + class="sidebar-item-icon is-active"> + </icon> </div> <div class="title hide-collapsed"> @@ -98,10 +101,12 @@ export default { v-if="isLocked" class="value sidebar-item-value" > - <i + <icon + name="lock" + :size="16" aria-hidden="true" - class="fa fa-lock sidebar-item-icon is-active" - ></i> + class="sidebar-item-icon inline is-active"> + </icon> {{ __('Locked') }} </div> @@ -109,10 +114,12 @@ export default { v-else class="no-value sidebar-item-value hide-collapsed" > - <i + <icon + name="lock-open" + :size="16" aria-hidden="true" - class="fa fa-unlock sidebar-item-icon" - ></i> + class="sidebar-item-icon inline"> + </icon> {{ __('Unlocked') }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 4ad3d469f25..25acc099699 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -3,6 +3,7 @@ import Store from '../../stores/sidebar_store'; import Mediator from '../../sidebar_mediator'; import eventHub from '../../event_hub'; import Flash from '../../../flash'; +import { __ } from '../../../locale'; import subscriptions from './subscriptions.vue'; export default { @@ -21,7 +22,7 @@ export default { onToggleSubscription() { this.mediator.toggleSubscription() .catch(() => { - Flash('Error occurred when toggling the notification subscription'); + Flash(__('Error occurred when toggling the notification subscription')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index a3a8213d63a..940e1764f3d 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -14,6 +14,10 @@ export default { type: Boolean, required: false, }, + id: { + type: Number, + required: false, + }, }, components: { loadingButton, @@ -32,7 +36,7 @@ export default { }, methods: { toggleSubscription() { - eventHub.$emit('toggleSubscription'); + eventHub.$emit('toggleSubscription', this.id); }, }, }; diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js deleted file mode 100644 index bb4d68fcd49..00000000000 --- a/app/assets/javascripts/subscription.js +++ /dev/null @@ -1,45 +0,0 @@ -class Subscription { - constructor(containerElm) { - this.containerElm = containerElm; - - const subscribeButton = containerElm.querySelector('.js-subscribe-button'); - if (subscribeButton) { - // remove class so we don't bind twice - subscribeButton.classList.remove('js-subscribe-button'); - subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); - } - } - - toggleSubscription(event) { - const button = event.currentTarget; - const buttonSpan = button.querySelector('span'); - if (!buttonSpan || button.classList.contains('disabled')) { - return; - } - button.classList.add('disabled'); - - const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; - const toggleActionUrl = this.containerElm.dataset.url; - - $.post(toggleActionUrl, () => { - button.classList.remove('disabled'); - - // hack to allow this to work with the issue boards Vue object - if (document.querySelector('html').classList.contains('issue-boards-page')) { - gl.issueBoards.boardStoreIssueSet( - 'subscribed', - !gl.issueBoards.BoardsStore.detail.issue.subscribed, - ); - } else { - buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; - } - }); - } - - static bindAll(selector) { - [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); - } -} - -window.gl = window.gl || {}; -window.gl.Subscription = Subscription; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 37e39ce5477..1ab4c2229ca 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,33 +1,24 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ +export default function subscriptionSelect() { + $('.js-subscription-event').each((i, element) => { + const fieldName = $(element).data('field-name'); -class SubscriptionSelect { - constructor() { - $('.js-subscription-event').each(function(i, el) { - var fieldName; - fieldName = $(el).data("field-name"); - return $(el).glDropdown({ - selectable: true, - fieldName: fieldName, - toggleLabel: (function(_this) { - return function(selected, el, instance) { - var $item, label; - label = 'Subscription'; - $item = instance.dropdown.find('.is-active'); - if ($item.length) { - label = $item.text(); - } - return label; - }; - })(this), - clicked: function(options) { - return options.e.preventDefault(); - }, - id: function(obj, el) { - return $(el).data("id"); + return $(element).glDropdown({ + selectable: true, + fieldName, + toggleLabel(selected, el, instance) { + let label = 'Subscription'; + const $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); } - }); + return label; + }, + clicked(options) { + return options.e.preventDefault(); + }, + id(obj, el) { + return $(el).data('id'); + }, }); - } + }); } - -window.SubscriptionSelect = SubscriptionSelect; diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 2e5f9f1088f..8f116233e72 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -6,10 +6,9 @@ Sample configuration: <icon - :img-src="userAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" + name="retry" + :size="32" + css-classes="top" /> */ diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 16c0a8efcd2..564fc5029af 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,4 +1,6 @@ <script> + import Icon from '../../../vue_shared/components/icon.vue'; + export default { props: { isLocked: { @@ -14,12 +16,16 @@ }, }, + components: { + Icon, + }, + computed: { - iconClass() { - return { - 'fa-eye-slash': this.isConfidential, - 'fa-lock': this.isLocked, - }; + warningIcon() { + if (this.isConfidential) return 'eye-slash'; + if (this.isLocked) return 'lock'; + + return ''; }, isLockedAndConfidential() { @@ -30,12 +36,13 @@ </script> <template> <div class="issuable-note-warning"> - <i - aria-hidden="true" - class="fa icon" - :class="iconClass" - v-if="!isLockedAndConfidential" - ></i> + <icon + :name="warningIcon" + :size="16" + class="icon inline" + aria-hidden="true" + v-if="!isLockedAndConfidential"> + </icon> <span v-if="isLockedAndConfidential"> {{ __('This issue is confidential and locked.') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index a873e00d0f3..ee50ce27c3d 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -25,6 +25,11 @@ type: String, required: false, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -129,6 +134,7 @@ <markdown-toolbar :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" + :can-attach-file="canAttachFile" /> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 70f5fc1d664..6c575d8eb49 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -50,7 +50,9 @@ <template> <div class="md-header"> <ul class="nav-links clearfix"> - <li :class="{ active: !previewMarkdown }"> + <li + class="md-header-tab" + :class="{ active: !previewMarkdown }"> <a class="js-write-link" href="#md-write-holder" @@ -59,7 +61,9 @@ Write </a> </li> - <li :class="{ active: previewMarkdown }"> + <li + class="md-header-tab" + :class="{ active: previewMarkdown }"> <a class="js-preview-link" href="#md-preview-holder" @@ -68,56 +72,52 @@ Preview </a> </li> - <li class="pull-right"> - <div class="toolbar-group"> - <toolbar-button - tag="**" - button-title="Add bold text" - icon="bold" /> - <toolbar-button - tag="*" - button-title="Add italic text" - icon="italic" /> - <toolbar-button - tag="> " - :prepend="true" - button-title="Insert a quote" - icon="quote" /> - <toolbar-button - tag="`" - tag-block="```" - button-title="Insert code" - icon="code" /> - <toolbar-button - tag="* " - :prepend="true" - button-title="Add a bullet list" - icon="list-bulleted" /> - <toolbar-button - tag="1. " - :prepend="true" - button-title="Add a numbered list" - icon="list-numbered" /> - <toolbar-button - tag="* [ ] " - :prepend="true" - button-title="Add a task list" - icon="task-done" /> - </div> - <div class="toolbar-group"> - <button - v-tooltip - aria-label="Go full screen" - class="toolbar-btn js-zen-enter" - data-container="body" - tabindex="-1" - title="Go full screen" - type="button"> - <icon - name="screen-full"> - </icon> - </button> - </div> + <li class="md-header-toolbar"> + <toolbar-button + tag="**" + button-title="Add bold text" + icon="bold" /> + <toolbar-button + tag="*" + button-title="Add italic text" + icon="italic" /> + <toolbar-button + tag="> " + :prepend="true" + button-title="Insert a quote" + icon="quote" /> + <toolbar-button + tag="`" + tag-block="```" + button-title="Insert code" + icon="code" /> + <toolbar-button + tag="* " + :prepend="true" + button-title="Add a bullet list" + icon="list-bulleted" /> + <toolbar-button + tag="1. " + :prepend="true" + button-title="Add a numbered list" + icon="list-numbered" /> + <toolbar-button + tag="* [ ] " + :prepend="true" + button-title="Add a task list" + icon="task-done" /> + <button + v-tooltip + aria-label="Go full screen" + class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" + data-container="body" + tabindex="-1" + title="Go full screen" + type="button"> + <icon + name="screen-full"> + </icon> + </button> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 65fe7bbd94e..ea2509d2839 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -9,6 +9,11 @@ type: String, required: false, }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> @@ -41,7 +46,10 @@ are supported </template> </div> - <span class="uploading-container"> + <span + v-if="canAttachFile" + class="uploading-container" + > <span class="uploading-progress-container hide"> <i class="fa fa-file-image-o toolbar-button-icon" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index b930fb116a3..e3e41f8f0ca 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -40,7 +40,7 @@ <button v-tooltip type="button" - class="toolbar-btn js-md hidden-xs" + class="toolbar-btn js-md" tabindex="-1" data-container="body" :data-md-tag="tag" diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 10f9e9b70b0..9982a5779af 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -56,6 +56,12 @@ } } +.blank-state-center { + padding-top: 20px; + padding-bottom: 20px; + text-align: center; +} + .blank-state { padding: 20px; border: 1px solid $border-color; @@ -66,7 +72,10 @@ align-items: center; padding: 50px 30px; } +} +.blank-state, +.blank-state-center { .blank-state-icon { svg { display: block; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5f5b5657a2f..5e4ddf366ef 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -2,7 +2,9 @@ .cgray { color: $common-gray; } .clgray { color: $common-gray-light; } .cred { color: $common-red; } +svg.cred { fill: $common-red; } .cgreen { color: $common-green; } +svg.cgreen { fill: $common-green; } .cdark { color: $common-gray-dark; } .text-secondary { color: $gl-text-color-secondary; @@ -428,6 +430,7 @@ img.emoji { /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-5 { margin-top: 5px; } +.prepend-top-8 { margin-top: $grid-size; } .prepend-top-10 { margin-top: 10px; } .prepend-top-15 { margin-top: 15px; } .prepend-top-default { margin-top: $gl-padding !important; } diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index 320f458630a..b73932eb7e1 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -40,12 +40,6 @@ a:hover { background-color: $link-hover-background; color: $gl-text-color; - - .settings-avatar { - svg { - fill: $gl-text-color; - } - } } .avatar-container { @@ -138,10 +132,6 @@ color: $gl-text-color-secondary; } - svg { - fill: $gl-text-color-secondary; - } - .nav-item-name { flex: 1; } @@ -224,10 +214,6 @@ &:hover { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } } @@ -338,7 +324,6 @@ align-items: center; svg { - fill: $gl-text-color-secondary; margin-right: 8px; } @@ -349,10 +334,6 @@ &:hover { background-color: $border-color; color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 74b6b31b07e..cf8165eab5b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -305,16 +305,11 @@ color: $gl-text-color; border-color: $dropdown-input-focus-border; outline: none; - - svg { - fill: $gl-text-color; - } } svg { height: 14px; width: 14px; - fill: $gl-text-color-secondary; vertical-align: middle; } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index dc591c06c88..db36e27fa74 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -30,10 +30,6 @@ &.dropdown.open > a { color: $color-900; background-color: $color-alternate; - - svg { - fill: currentColor; - } } &.line-separator { @@ -51,10 +47,6 @@ color: $color-200; > a { - svg { - fill: $color-200; - } - &.header-user-dropdown-toggle { .header-user-avatar { border-color: $color-200; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2218b5705fc..f985a3aea5c 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -235,10 +235,6 @@ opacity: 1; color: $white-light; - svg { - fill: currentColor; - } - &.header-user-dropdown-toggle .header-user-avatar { border-color: $white-light; } @@ -269,14 +265,6 @@ font-size: 20px; } } - - &.active > a, - &.dropdown.open > a { - - svg { - fill: currentColor; - } - } } } } @@ -289,10 +277,6 @@ text-decoration: none; outline: 0; color: $white-light; - - svg { - fill: currentColor; - } } > a { @@ -307,10 +291,6 @@ border-radius: $border-radius-default; height: 32px; font-weight: $gl-font-weight-bold; - - svg { - fill: currentColor; - } } &.line-separator { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index ef864e8f6a9..1ab5e6a93f9 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -1,63 +1,35 @@ .ci-status-icon-success, .ci-status-icon-passed { color: $green-500; - - svg { - fill: $green-500; - } } .ci-status-icon-failed { color: $gl-danger; - - svg { - fill: $gl-danger; - } } .ci-status-icon-pending, .ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { color: $orange-500; - - svg { - fill: $orange-500; - } } .ci-status-icon-running { color: $blue-400; - - svg { - fill: $blue-400; - } } .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } .ci-status-icon-created, .ci-status-icon-skipped { color: $gray-darkest; - - svg { - fill: $gray-darkest; - } } .ci-status-icon-manual { color: $gl-text-color; - - svg { - fill: $gl-text-color; - } } .icon-link { diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 6819fd88b7f..78a8e57ddbb 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -27,6 +27,8 @@ } svg { + fill: currentColor; + &.s8 { @include svg-size(8px); } &.s12 { @include svg-size(12px); } &.s16 { @include svg-size(16px); } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index cd6f94fb354..5389eb0a5f2 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -57,6 +57,7 @@ .md-header { .nav-links { a { + width: 100%; padding-top: 0; line-height: 19px; @@ -72,6 +73,28 @@ } } +.md-header-tab { + @media(max-width: $screen-xs-max) { + flex: 1; + width: 100%; + border-bottom: 1px solid $border-color; + text-align: center; + } +} + +.md-header-toolbar { + margin-left: auto; + + @media(max-width: $screen-xs-max) { + flex: none; + display: flex; + justify-content: center; + width: 100%; + padding-top: $gl-padding-top; + padding-bottom: $gl-padding-top; + } +} + .referenced-users { color: $gl-text-color; padding-top: 10px; @@ -126,16 +149,6 @@ } } -.toolbar-group { - float: left; - margin-right: -5px; - margin-left: $gl-padding; - - &:first-child { - margin-left: 0; - } -} - .toolbar-btn { float: left; padding: 0 7px; @@ -158,6 +171,16 @@ } } +.toolbar-fullscreen-btn { + margin-left: $gl-padding; + margin-right: -5px; + + @media(max-width: $screen-xs-max) { + margin-left: 0; + margin-right: 0; + } +} + .atwho-view { overflow-y: auto; overflow-x: hidden; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 33012133b66..e12b5aab381 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -130,14 +130,6 @@ background-color: $color-light; color: $color-dark; border-color: $color-dark; - - svg { - fill: $color-dark; - } - } - - svg { - fill: $color-main; } } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 5a4d3ba0ee9..dbd3144b9b4 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -57,15 +57,7 @@ padding: 5px; font-size: 36px; - svg { - fill: $gl-text-color; - } - &:hover { color: $black; - - svg { - fill: $black; - } } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 27b10b536a2..f139f4ab650 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -49,6 +49,7 @@ font-size: 12px; border-radius: 0; border: 0; + padding: $grid-size; .bash { display: block; @@ -57,14 +58,13 @@ .top-bar { height: 35px; - display: flex; - justify-content: flex-end; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; position: sticky; position: -webkit-sticky; top: $header-height; + padding: $grid-size; &.affix { top: $header-height; @@ -90,9 +90,6 @@ } .truncated-info { - margin: 0 auto; - align-self: center; - .truncated-info-size { margin: 0 5px; } @@ -118,7 +115,11 @@ .controllers-buttons { color: $gl-text-color; - margin: 0 10px; + margin: 0 $grid-size; + + &:last-child { + margin-right: 0; + } } .btn-scroll.animate { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index bce94e09367..848d7f144dc 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -628,21 +628,46 @@ } .diff-file-changes { - width: 450px; + max-width: 560px; + width: 100%; z-index: 150; @media (min-width: $screen-sm-min) { left: $gl-padding; } - a { + .diff-changed-file { + display: flex; padding-top: 8px; padding-bottom: 8px; + min-width: 0; } - .diff-changed-file { + .diff-file-changed-icon { + margin-top: 2px; + } + + .diff-changed-file-content { display: flex; - align-items: center; + flex-direction: column; + min-width: 0; + } + + .diff-changed-file-name, + .diff-changed-file-path { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .diff-changed-file-path { + direction: rtl; + color: $gl-text-color-tertiary; + } + + .diff-changed-stats { + margin-left: auto; + white-space: nowrap; } } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index dae8ccdef6c..9cc9e11bcd1 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -1,23 +1,3 @@ -.documentation-index { - h1 { - margin: 0; - } - - h2 { - font-size: 20px; - } - - li { - line-height: 24px; - color: $document-index-color; - - a { - margin-right: 3px; - } - } -} - - .shortcut-mappings { font-size: 12px; color: $help-shortcut-mapping-color; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 760c7c80aff..7a5dab16561 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -6,28 +6,20 @@ } .issuable-warning-icon { - color: $orange-600; background-color: $orange-100; border-radius: $border-radius-default; - padding: 5px; margin: 0 $btn-side-margin 0 0; width: $issuable-warning-size; height: $issuable-warning-size; text-align: center; - &:first-of-type { - margin-right: $issuable-warning-icon-margin; + .icon { + fill: $orange-600; + vertical-align: text-bottom; } -} -.sidebar-item-icon { - border-radius: $border-radius-default; - padding: 5px; - margin: 0 3px 0 -4px; - - &.is-active { - color: $orange-600; - background-color: $orange-50; + &:first-of-type { + margin-right: $issuable-warning-icon-margin; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 89f93a92f2e..1e6992cb65e 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -113,6 +113,8 @@ .icon { margin-right: $issuable-warning-icon-margin; + vertical-align: text-bottom; + fill: $orange-600; } + .md-area { @@ -137,12 +139,24 @@ } } -.sidebar-item-value { - .fa { - background-color: inherit; +.sidebar-item-icon { + border-radius: $border-radius-default; + margin: 0 3px 0 -4px; + vertical-align: middle; + + &.is-active { + fill: $orange-600; } } +.sidebar-collapsed-icon .sidebar-item-icon { + margin: 0; +} + +.sidebar-item-value .sidebar-item-icon { + fill: $theme-gray-700; +} + .sidebar-item-warning-message { line-height: 1.5; padding: 16px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 9537eeeee97..2461b818219 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -547,10 +547,6 @@ ul.notes { width: 16px; top: 0; vertical-align: text-top; - - path { - fill: currentColor; - } } .award-control-icon-positive, @@ -570,10 +566,6 @@ ul.notes { .link-highlight { color: $gl-link-color; fill: $gl-link-color; - - svg { - fill: $gl-link-color; - } } .award-control-icon-neutral { diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 25c80e1f950..ade5ddd147b 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -55,10 +55,6 @@ &:not(span):hover { background-color: rgba($gl-text-color-secondary, .07); } - - svg { - fill: $gl-text-color-secondary; - } } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3be7aee69bc..b2ec491146f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :authenticate_user_from_personal_access_token! - before_action :authenticate_user_from_rss_token! + before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :validate_user_service_ticket! before_action :check_password_expiration @@ -97,30 +96,15 @@ class ApplicationController < ActionController::Base # (e.g. tokens) to authenticate the user, whereas Devise sets current_user def auth_user return current_user if current_user.present? + return try(:authenticated_user) end - def authenticate_user_from_personal_access_token! - token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence - - return unless token.present? - - user = User.find_by_personal_access_token(token) + # This filter handles personal access tokens, and atom requests with rss tokens + def authenticate_sessionless_user! + user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user - sessionless_sign_in(user) - end - - # This filter handles authentication for atom request with an rss_token - def authenticate_user_from_rss_token! - return unless request.format.atom? - - token = params[:rss_token].presence - - return unless token.present? - - user = User.find_by_rss_token(token) - - sessionless_sign_in(user) + sessionless_sign_in(user) if user end def log_exception(exception) @@ -212,7 +196,11 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + return if session[:impersonator_id] || current_user&.ldap_user? + + password_expires_at = current_user&.password_expires_at + + if password_expires_at && password_expires_at < Time.now return redirect_to new_profile_password_path end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 10e8e54f402..cde1e284d2d 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -44,6 +44,7 @@ class AutocompleteController < ApplicationController if @project.blank? && params[:group_id].present? group = Group.find(params[:group_id]) return render_404 unless can?(current_user, :read_group, group) + group end end @@ -54,6 +55,7 @@ class AutocompleteController < ApplicationController if params[:project_id].present? project = Project.find(params[:project_id]) return render_404 unless can?(current_user, :read_project, project) + project end end diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 737656b3dcc..f8049b20b9f 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -84,6 +84,7 @@ module Boards resource.as_json( only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], labels: true, + sidebar_endpoints: true, include: { project: { only: [:id, :path] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 3c64fd964ff..be2e1b47feb 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -4,7 +4,7 @@ module NotesActions included do before_action :set_polling_interval_header, only: [:index] - before_action :noteable, only: :index + before_action :require_noteable!, only: [:index, :create] before_action :authorize_admin_note!, only: [:update, :destroy] before_action :note_project, only: [:create] end @@ -90,7 +90,7 @@ module NotesActions if note.persisted? attrs[:valid] = true - if noteable.nil? || noteable.discussions_rendered_on_frontend? + if noteable.discussions_rendered_on_frontend? attrs.merge!(note_serializer.represent(note)) else attrs.merge!( @@ -191,7 +191,11 @@ module NotesActions end def noteable - @noteable ||= notes_finder.target || render_404 + @noteable ||= notes_finder.target || @note&.noteable + end + + def require_noteable! + render_404 unless noteable end def last_fetched_at diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 7a7bcb1a3d2..f013d21275e 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -80,7 +80,8 @@ class Groups::MilestonesController < Groups::ApplicationController milestones = MilestonesFinder.new(search_params).execute legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) - milestones + legacy_milestones + @sort = params[:sort] || 'due_date_asc' + MilestoneArray.sort(milestones + legacy_milestones, @sort) end def milestone diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 510813846a4..567957ba2cb 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -4,6 +4,7 @@ class Import::GitlabProjectsController < Import::BaseController def new @namespace = Namespace.find(project_params[:namespace_id]) return render_404 unless current_user.can?(:create_projects, @namespace) + @path = project_params[:path] end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 9612b8d8514..56baa19f864 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -54,7 +54,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if current_user log_audit_event(current_user, with: :saml) # Update SAML identity if data has changed. - identity = current_user.identities.find_by(extern_uid: oauth['uid'], provider: :saml) + identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take if identity.nil? current_user.identities.create(extern_uid: oauth['uid'], provider: :saml) redirect_to profile_account_path, notice: 'Authentication method updated' @@ -98,7 +98,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_omniauth if current_user # Add new authentication method - current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) + current_user.identities + .with_extern_uid(oauth['provider'], oauth['uid']) + .first_or_create(extern_uid: oauth['uid']) log_audit_event(current_user, with: oauth['provider']) redirect_to profile_account_path, notice: 'Authentication method updated' else diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 494d412b532..6ff96a3f295 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -22,12 +22,7 @@ class Projects::CommitController < Projects::ApplicationController apply_diff_view_cookie! respond_to do |format| - format.html do - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599 - Gitlab::GitalyClient.allow_n_plus_1_calls do - render - end - end + format.html { render } format.diff { render text: @commit.to_diff } format.patch { render text: @commit.to_patch } end @@ -112,7 +107,7 @@ class Projects::CommitController < Projects::ApplicationController end def commit - @noteable = @commit ||= @project.commit(params[:id]) + @noteable = @commit ||= @project.commit_by(oid: params[:id]) end def define_commit_vars diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 28920877635..5f4afd2cdee 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -57,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end + @commits = @commits.with_pipeline_status @commits = prepare_commits_for_rendering(@commits) end end diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 47c312ffddf..1a418d0f15a 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -12,6 +12,7 @@ class Projects::DeploymentsController < Projects::ApplicationController def metrics return render_404 unless deployment.has_metrics? + @metrics = deployment.metrics if @metrics&.any? render json: @metrics, status: :ok diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index dbc1c8bcc28..f58ee3e9109 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -12,6 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController if group return render_404 unless can?(current_user, :read_group, group) + Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) else flash[:alert] = 'Please select a group.' diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index dbc9106ba6d..28fee0465d5 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -171,6 +171,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue return @issue if defined?(@issue) + # The Sortable default scope causes performance issues when used with find_by @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @note = @project.notes.new(noteable: @issuable) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 480a2dff262..e0f4710175f 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -111,6 +111,7 @@ class Projects::LabelsController < Projects::ApplicationController begin return render_404 unless promote_service.execute(@label) + respond_to do |format| format.html do redirect_to(project_labels_path(@project), diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 32759672b6c..293869345bd 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -54,6 +54,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController name = request.headers['X-Gitlab-Lfs-Tmp'] return if name.include?('/') return unless oid.present? && name.start_with?(oid) + name end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 7d16e77ef66..d60a24d3f1d 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -10,10 +10,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def show @environment = @merge_request.environments_for(current_user).last - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431 - Gitlab::GitalyClient.allow_n_plus_1_calls do - render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } - end + render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") } end def diff_for_path diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 22de6680511..abe4e5245b1 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo def commits # Get commits from repository # or from cache if already merged - @commits = prepare_commits_for_rendering(@merge_request.commits) + @commits = + prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status) render json: { html: view_to_html_string('projects/merge_requests/_commits') } end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index ef7d047b1ad..627cb2bd93c 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -76,6 +76,7 @@ class Projects::NotesController < Projects::ApplicationController def authorize_create_note! return unless noteable.lockable? + access_denied! unless can?(current_user, :create_note, noteable) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index f7a9c98629d..292e4158f8b 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -28,6 +28,7 @@ class Projects::WikisController < Projects::ApplicationController ) else return render('empty') unless can?(current_user, :create_wiki, @project) + @page = WikiPage.new(@project_wiki) @page.title = params[:id] @@ -74,7 +75,11 @@ class Projects::WikisController < Projects::ApplicationController def history @page = @project_wiki.find_page(params[:id]) - unless @page + if @page + @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]), + total_count: @page.count_versions) + .page(params[:page]) + else redirect_to( project_wiki_path(@project, :home), notice: "Page not found" @@ -101,7 +106,7 @@ class Projects::WikisController < Projects::ApplicationController # Call #wiki to make sure the Wiki Repo is initialized @project_wiki.wiki - @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15)) + @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15)) rescue ProjectWiki::CouldNotCreateWikiError flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." redirect_to project_path(@project) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2a473ec0cec..a784c6f402a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -269,6 +269,7 @@ class ProjectsController < Projects::ApplicationController def render_landing_page if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + render 'projects/empty' if @project.empty_repo? else if @project.wiki_enabled? diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index f9496787b15..c8b4682e6dc 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -20,6 +20,7 @@ class Snippets::NotesController < ApplicationController def snippet PersonalSnippet.find_by(id: params[:snippet_id]) end + alias_method :noteable, :snippet def note_params super.merge(noteable_id: params[:snippet_id]) diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 760166b453f..d975f354a88 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -18,6 +18,7 @@ class PersonalAccessTokensFinder def by_user(tokens) return tokens unless @params[:user] + tokens.where(user: @params[:user]) end diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 8ad94d3f723..df590cf47c8 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -30,4 +30,11 @@ module AppearancesHelper render 'shared/logo.svg' end end + + # Skip the 'GitLab' type logo when custom brand logo is set + def brand_header_logo_type + unless brand_item && brand_item.header_logo? + render 'shared/logo_type.svg' + end + end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index cd1ecaadb85..e5d2693b01e 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -231,6 +231,15 @@ module ApplicationSettingsHelper :sign_in_text, :signup_enabled, :terminal_max_session_time, + :throttle_unauthenticated_enabled, + :throttle_unauthenticated_requests_per_period, + :throttle_unauthenticated_period_in_seconds, + :throttle_authenticated_web_enabled, + :throttle_authenticated_web_requests_per_period, + :throttle_authenticated_web_period_in_seconds, + :throttle_authenticated_api_enabled, + :throttle_authenticated_api_requests_per_period, + :throttle_authenticated_api_period_in_seconds, :two_factor_grace_period, :unique_ips_limit_enabled, :unique_ips_limit_per_user, diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 4dd573c61f1..636316da80a 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -6,11 +6,6 @@ # See 'detailed_status?` method and `Gitlab::Ci::Status` module. # module CiStatusHelper - def ci_status_path(pipeline) - project = pipeline.project - project_pipeline_path(project, pipeline) - end - def ci_label_for_status(status) if detailed_status?(status) return status.label diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 4e4a66e8a02..e82136f0177 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -111,6 +111,7 @@ module DiffHelper def diff_file_old_blob_raw_path(diff_file) sha = diff_file.old_content_sha return unless sha + project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) end @@ -152,11 +153,11 @@ module DiffHelper def diff_file_changed_icon(diff_file) if diff_file.deleted_file? || diff_file.renamed_file? - "minus" + "file-deletion" elsif diff_file.new_file? - "plus" + "file-addition" else - "adjust" + "file-modified" end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 5f11fe62030..878bc9b5c9c 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -24,6 +24,7 @@ module EmailsHelper def action_title(url) return unless url + %w(merge_requests issues commit).each do |action| if url.split("/").include?(action) return "View #{action.humanize.singularize}" diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 2c85d7d7720..9d269cb65d6 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -53,6 +53,7 @@ module MarkupHelper # text, wrapping anything found in the requested link fragment.children.each do |node| next unless node.text? + node.replace(link_to(node.text, url, html_options)) end end @@ -221,7 +222,7 @@ module MarkupHelper data = options[:data].merge({ container: 'body' }) content_tag :button, type: 'button', - class: 'toolbar-btn js-md has-tooltip hidden-xs', + class: 'toolbar-btn js-md has-tooltip', tabindex: -1, data: data, title: options[:title], diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index fde961e2da4..3e42063224e 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -78,6 +78,7 @@ module NotificationsHelper # Create hidden field to send notification setting source to controller def hidden_setting_source_input(notification_setting) return unless notification_setting.source_type + hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cf28a917fd1..2f57660516d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -140,7 +140,8 @@ module SearchHelper placeholder: 'Search or filter results...', data: { 'username-params' => @users.to_json(only: [:id, :username]) - } + }, + autocomplete: 'off' } if @project.present? diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 0e106e2c85d..5b2ea38a03d 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -97,6 +97,7 @@ module TreeHelper part_path = part if part_path.empty? next if parts.count > max_links && !parts.last(2).include?(part) + yield(part, part_path) end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 46867d2d974..c3d5628f241 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -150,6 +150,7 @@ module VisibilityLevelHelper def restricted_visibility_levels(show_all = false) return [] if current_user.admin? && !show_all + current_application_settings.restricted_visibility_levels || [] end @@ -159,6 +160,7 @@ module VisibilityLevelHelper def disallowed_visibility_level?(form_model, level) return false unless form_model.respond_to?(:visibility_level_allowed?) + !form_model.visibility_level_allowed?(level) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 5e16badabec..a7e0219b03a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -295,6 +295,15 @@ class ApplicationSetting < ActiveRecord::Base sign_in_text: nil, signup_enabled: Settings.gitlab['signup_enabled'], terminal_max_session_time: 0, + throttle_unauthenticated_enabled: false, + throttle_unauthenticated_requests_per_period: 3600, + throttle_unauthenticated_period_in_seconds: 3600, + throttle_authenticated_web_enabled: false, + throttle_authenticated_web_requests_per_period: 7200, + throttle_authenticated_web_period_in_seconds: 3600, + throttle_authenticated_api_enabled: false, + throttle_authenticated_api_requests_per_period: 7200, + throttle_authenticated_api_period_in_seconds: 3600, two_factor_grace_period: 48, user_default_external: false, polling_interval_multiplier: 1, diff --git a/app/models/blob.rb b/app/models/blob.rb index ad0bc2e2ead..29e762724e3 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -76,12 +76,24 @@ class Blob < SimpleDelegator new(blob, project) end + def self.lazy(project, commit_id, path) + BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader| + project.repository.blobs_at(items.map(&:values)).each do |blob| + loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob + end + end + end + def initialize(blob, project = nil) @project = project super(blob) end + def inspect + "#<#{self.class.name} oid:#{id[0..8]} commit:#{commit_id[0..8]} path:#{path}>" + end + # Returns the data of the blob. # # If the blob is a text based blob the content is converted to UTF-8 and any @@ -95,7 +107,10 @@ class Blob < SimpleDelegator end def load_all_data! - super(project.repository) if project + # Endpoint needed: gitlab-org/gitaly#756 + Gitlab::GitalyClient.allow_n_plus_1_calls do + super(project.repository) if project + end end def no_highlighting? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1b2b0d17910..1d9f367183e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -317,6 +317,7 @@ module Ci def execute_hooks return unless project + build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :job_hooks) project.execute_services(build_data.dup, :job_hooks) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 19814864e50..ebbefc51a4f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -149,34 +149,70 @@ module Ci end end - # ref can't be HEAD or SHA, can only be branch/tag name - scope :latest, ->(ref = nil) do - max_id = unscope(:select) - .select("max(#{quoted_table_name}.id)") - .group(:ref, :sha) - - if ref - where(ref: ref, id: max_id.where(ref: ref)) - else - where(id: max_id) - end - end scope :internal, -> { where(source: internal_sources) } + # Returns the pipelines in descending order (= newest first), optionally + # limited to a number of references. + # + # ref - The name (or names) of the branch(es)/tag(s) to limit the list of + # pipelines to. + def self.newest_first(ref = nil) + relation = order(id: :desc) + + ref ? relation.where(ref: ref) : relation + end + def self.latest_status(ref = nil) - latest(ref).status + newest_first(ref).pluck(:status).first end def self.latest_successful_for(ref) - success.latest(ref).order(id: :desc).first + newest_first(ref).success.take end def self.latest_successful_for_refs(refs) - success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash| + relation = newest_first(refs).success + + relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline end end + # Returns a Hash containing the latest pipeline status for every given + # commit. + # + # The keys of this Hash are the commit SHAs, the values the statuses. + # + # commits - The list of commit SHAs to get the status for. + # ref - The ref to scope the data to (e.g. "master"). If the ref is not + # given we simply get the latest status for the commits, regardless + # of what refs their pipelines belong to. + def self.latest_status_per_commit(commits, ref = nil) + p1 = arel_table + p2 = arel_table.alias + + # This LEFT JOIN will filter out all but the newest row for every + # combination of (project_id, sha) or (project_id, sha, ref) if a ref is + # given. + cond = p1[:sha].eq(p2[:sha]) + .and(p1[:project_id].eq(p2[:project_id])) + .and(p1[:id].lt(p2[:id])) + + cond = cond.and(p1[:ref].eq(p2[:ref])) if ref + join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond) + + relation = select(:sha, :status) + .where(sha: commits) + .where(p2[:id].eq(nil)) + .joins(join.join_sources) + + relation = relation.where(ref: ref) if ref + + relation.each_with_object({}) do |row, hash| + hash[row[:sha]] = row[:status] + end + end + def self.truncate_sha(sha) sha[0...8] end @@ -300,8 +336,10 @@ module Ci def latest? return false unless ref + commit = project.commit(ref) return false unless commit + commit.sha == sha end @@ -469,7 +507,10 @@ module Ci end def latest_builds_with_artifacts - @latest_builds_with_artifacts ||= builds.latest.with_artifacts + # We purposely cast the builds to an Array here. Because we always use the + # rows if there are more than 0 this prevents us from having to run two + # queries: one to get the count and one to get the rows. + @latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a end private diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index ee2e43ee9dd..7fac32466ab 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -56,6 +56,7 @@ module Clusters before_transition any => [:creating] do |provider, transition| operation_id = transition.args.first raise ArgumentError.new('operation_id is required') unless operation_id.present? + provider.operation_id = operation_id end diff --git a/app/models/commit.rb b/app/models/commit.rb index 6dba154a6ea..8401d99a08f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -80,10 +80,11 @@ class Commit @raw = raw_commit @project = project + @statuses = {} end def id - @raw.id + raw.id end def ==(other) @@ -236,11 +237,13 @@ class Commit end def status(ref = nil) - @statuses ||= {} - return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = pipelines.latest_status(ref) + @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id] + end + + def set_status_for_ref(ref, status) + @statuses[ref] = status end def signature @@ -358,7 +361,7 @@ class Commit @deltas ||= raw.deltas end - def diffs(diff_options = nil) + def diffs(diff_options = {}) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb new file mode 100644 index 00000000000..dd93af9df64 --- /dev/null +++ b/app/models/commit_collection.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# A collection of Commit instances for a specific project and Git reference. +class CommitCollection + include Enumerable + + attr_reader :project, :ref, :commits + + # project - The project the commits belong to. + # commits - The Commit instances to store. + # ref - The name of the ref (e.g. "master"). + def initialize(project, commits, ref = nil) + @project = project + @commits = commits + @ref = ref + end + + def each(&block) + commits.each(&block) + end + + # Sets the pipeline status for every commit. + # + # Setting this status ahead of time removes the need for running a query for + # every commit we're displaying. + def with_pipeline_status + statuses = project.pipelines.latest_status_per_commit(map(&:id), ref) + + each do |commit| + commit.set_status_for_ref(ref, statuses[commit.id]) + end + + self + end + + def respond_to_missing?(message, inc_private = false) + commits.respond_to?(message, inc_private) + end + + # rubocop:disable GitlabSecurity/PublicSend + def method_missing(message, *args, &block) + commits.public_send(message, *args, &block) + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 9adc309a22b..d8394415362 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -98,6 +98,7 @@ module Awardable def create_award_emoji(name, current_user) return unless emoji_awardable? + award_emoji.create(name: normalize_name(name), user: current_user) end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb index 6a9b52a1ef8..eb9417dc34f 100644 --- a/app/models/fork_network_member.rb +++ b/app/models/fork_network_member.rb @@ -4,4 +4,14 @@ class ForkNetworkMember < ActiveRecord::Base belongs_to :forked_from_project, class_name: 'Project' validates :fork_network, :project, presence: true + + after_destroy :cleanup_fork_network + + private + + def cleanup_fork_network + # Explicitly using `#count` makes sure we have the correct number if the + # relation was loaded in the fork_network. + fork_network.destroy if fork_network.fork_network_members.count == 0 + end end diff --git a/app/models/identity.rb b/app/models/identity.rb index ac8094b610e..ff811e19f8a 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,18 +1,27 @@ class Identity < ActiveRecord::Base include Sortable include CaseSensitivity + belongs_to :user validates :provider, presence: true - validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider } + validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false } validates :user_id, uniqueness: { scope: :provider } + scope :with_provider, ->(provider) { where(provider: provider) } scope :with_extern_uid, ->(provider, extern_uid) do - extern_uid = Gitlab::LDAP::Person.normalize_dn(extern_uid) if provider.starts_with?('ldap') - where(extern_uid: extern_uid, provider: provider) + iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider) end def ldap? provider.starts_with?('ldap') end + + def self.normalize_uid(provider, uid) + if provider.to_s.starts_with?('ldap') + Gitlab::LDAP::Person.normalize_dn(uid) + else + uid.to_s + end + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index b5abc8f57b0..a9863a50d84 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -246,7 +246,12 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| - json[:subscribed] = subscribed?(options[:user], project) if options.key?(:user) && options[:user] + if options.key?(:sidebar_endpoints) && project + url_helper = Gitlab::Routing.url_helpers + + json.merge!(issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), + toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)) + end if options.key?(:labels) json[:labels] = labels.as_json( diff --git a/app/models/key.rb b/app/models/key.rb index f119b15c737..815fd1de909 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -27,8 +27,10 @@ class Key < ActiveRecord::Base after_commit :add_to_shell, on: :create after_create :post_create_hook + after_create :refresh_user_cache after_commit :remove_from_shell, on: :destroy after_destroy :post_destroy_hook + after_destroy :refresh_user_cache def key=(value) value&.delete!("\n\r") @@ -76,6 +78,12 @@ class Key < ActiveRecord::Base ) end + def refresh_user_cache + return unless user + + Users::KeysCountService.new(user).refresh_cache + end + def post_destroy_hook SystemHooksService.new.execute_hooks_for(self, :destroy) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 1eda0f9cbbd..5382f5cc627 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -284,8 +284,10 @@ class MergeRequestDiff < ActiveRecord::Base def load_commits commits = st_commits.presence || merge_request_diff_commits + commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) } - commits.map { |commit| Commit.from_hash(commit.to_hash, project) } + CommitCollection + .new(merge_request.source_project, commits, merge_request.source_branch) end def save_diffs diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 47e6b785c39..e01e52131f0 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -256,7 +256,7 @@ class Milestone < ActiveRecord::Base def start_date_should_be_less_than_due_date if due_date <= start_date - errors.add(:start_date, "Can't be greater than due date") + errors.add(:due_date, "must be greater than start date") end end diff --git a/app/models/note.rb b/app/models/note.rb index f9676361072..50c9caf8529 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -110,6 +110,7 @@ class Note < ActiveRecord::Base includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end + scope :with_metadata, -> { includes(:system_note_metadata) } after_initialize :ensure_discussion_id before_validation :nullify_blank_type, :nullify_blank_line_code @@ -169,7 +170,13 @@ class Note < ActiveRecord::Base end def cross_reference? - system? && matches_cross_reference_regex? + return unless system? + + if force_cross_reference_regex_check? + matches_cross_reference_regex? + else + SystemNoteService.cross_reference?(note) + end end def diff_note? @@ -382,4 +389,10 @@ class Note < ActiveRecord::Base def set_discussion_id self.discussion_id ||= discussion_class.discussion_id(self) end + + def force_cross_reference_regex_check? + return unless system? + + SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action) + end end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 43c77f3f2a2..8de42ff9d2e 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -65,6 +65,7 @@ class PagesDomain < ActiveRecord::Base def expired? return false unless x509 + current = Time.new current < x509.not_before || x509.not_after < current end @@ -75,6 +76,7 @@ class PagesDomain < ActiveRecord::Base def subject return unless x509 + x509.subject.to_s end @@ -102,6 +104,7 @@ class PagesDomain < ActiveRecord::Base def validate_pages_domain return unless domain + if domain.downcase.ends_with?(Settings.pages.host.downcase) self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") end @@ -109,6 +112,7 @@ class PagesDomain < ActiveRecord::Base def x509 return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) rescue OpenSSL::X509::CertificateError nil @@ -116,6 +120,7 @@ class PagesDomain < ActiveRecord::Base def pkey return unless key + @pkey ||= OpenSSL::PKey::RSA.new(key) rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError nil diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 976d85246a8..768f0a7472e 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -51,8 +51,10 @@ class HipchatService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + message = create_message(data) return unless message.present? + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index b487378edd2..1c065e1ddbd 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -176,6 +176,7 @@ class JiraService < IssueTrackerService def test_settings return unless client_url.present? + # Test settings by getting the project jira_request { client.ServerInfo.all.attrs } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 5080acffb3c..bc62972dbb0 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -182,6 +182,7 @@ class KubernetesService < DeploymentService kubeclient.get_pods(namespace: actual_namespace).as_json rescue KubeException => err raise err unless err.error_code == 404 + [] end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 3eecbea8cbf..a0af749a93f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -76,8 +76,8 @@ class ProjectWiki # Returns an Array of Gitlab WikiPage instances or an # empty Array if this Wiki has no pages. - def pages - wiki.pages.map { |page| WikiPage.new(self, page, true) } + def pages(limit: nil) + wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } end # Finds a page within the repository based on a tile diff --git a/app/models/repository.rb b/app/models/repository.rb index 3a89fa9264b..2bf21cbdcc4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -132,7 +132,8 @@ class Repository commits = Gitlab::Git::Commit.where(options) commits = Commit.decorate(commits, @project) if commits.present? - commits + + CommitCollection.new(project, commits, ref) end def commits_between(from, to) @@ -148,11 +149,14 @@ class Repository end raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled| - if is_enabled - find_commits_by_message_by_gitaly(query, ref, path, limit, offset) - else - find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) - end + commits = + if is_enabled + find_commits_by_message_by_gitaly(query, ref, path, limit, offset) + else + find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) + end + + CommitCollection.new(project, commits, ref) end end @@ -213,11 +217,7 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - @branch_exists_memo ||= Hash.new do |hash, key| - hash[key] = raw_repository.branch_exists?(key) - end - - @branch_exists_memo[branch_name] + branch_names.include?(branch_name) end def ref_exists?(ref) @@ -242,6 +242,7 @@ class Repository Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end end @@ -473,6 +474,11 @@ class Repository nil end + # items is an Array like: [[oid, path], [oid1, path1]] + def blobs_at(items) + raw_repository.batch_blobs(items).map { |blob| Blob.decorate(blob, project) } + end + def root_ref if raw_repository raw_repository.root_ref @@ -662,6 +668,7 @@ class Repository def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name + result = n.match(/\A#{name}-([0-9]+)\z/) result[1].to_i if result end.compact @@ -990,10 +997,6 @@ class Repository raw_repository.ls_files(actual_ref) end - def gitattribute(path, name) - raw_repository.attributes(path)[name] - end - def copy_gitattributes(ref) actual_ref = ref || root_ref begin diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 1f9f8d7286b..29035480371 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -1,4 +1,14 @@ class SystemNoteMetadata < ActiveRecord::Base + # These notes's action text might contain a reference that is external. + # We should always force a deep validation upon references that are found + # in this note type. + # Other notes can always be safely shown as all its references are + # in the same project (i.e. with the same permissions) + TYPES_WITH_CROSS_REFERENCES = %w[ + commit cross_reference + close duplicate + ].freeze + ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved diff --git a/app/models/user.rb b/app/models/user.rb index ea10e2854d6..f98165754ca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -170,6 +170,7 @@ class User < ActiveRecord::Base after_save :ensure_namespace_correct after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook + after_destroy :remove_key_cache after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') } after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } @@ -268,8 +269,7 @@ class User < ActiveRecord::Base end def for_github_id(id) - joins(:identities) - .where(identities: { provider: :github, extern_uid: id.to_s }) + joins(:identities).merge(Identity.with_extern_uid(:github, id)) end # Find a User by their primary email or any associated secondary email @@ -445,6 +445,10 @@ class User < ActiveRecord::Base skip_confirmation! if bool end + def skip_reconfirmation=(bool) + skip_reconfirmation! if bool + end + def generate_reset_token @reset_token, enc = Devise.token_generator.generate(self.class, :reset_password_token) @@ -624,7 +628,9 @@ class User < ActiveRecord::Base end def require_ssh_key? - keys.count == 0 && Gitlab::ProtocolAccess.allowed?('ssh') + count = Users::KeysCountService.new(self).count + + count.zero? && Gitlab::ProtocolAccess.allowed?('ssh') end def require_password_creation? @@ -886,6 +892,10 @@ class User < ActiveRecord::Base system_hook_service.execute_hooks_for(self, :destroy) end + def remove_key_cache + Users::KeysCountService.new(self).delete_cache + end + def delete_async(deleted_by:, params: {}) block if params[:hard_delete] DeleteUserWorker.perform_async(deleted_by.id, id, params) @@ -1119,6 +1129,7 @@ class User < ActiveRecord::Base # override, from Devise::Validatable def password_required? return false if internal? + super end @@ -1136,6 +1147,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 5f710961f95..bdfef677ef3 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -127,19 +127,24 @@ class WikiPage @version ||= @page.version end - # Returns an array of Gitlab Commit instances. - def versions + def versions(options = {}) return [] unless persisted? - wiki.wiki.page_versions(@page.path) + wiki.wiki.page_versions(@page.path, options) end - def commit - versions.first + def count_versions + return [] unless persisted? + + wiki.wiki.count_page_versions(@page.path) + end + + def last_version + @last_version ||= versions(limit: 1).first end def last_commit_sha - commit&.sha + last_version&.sha end # Returns the Date that this latest version was @@ -151,7 +156,7 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? && versions.first.sha != version.sha + @page.historical? && last_version.sha != version.sha end # Returns boolean True or False if this instance diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb new file mode 100644 index 00000000000..99cc9a196e6 --- /dev/null +++ b/app/services/base_count_service.rb @@ -0,0 +1,34 @@ +# Base class for services that count a single resource such as the number of +# issues for a project. +class BaseCountService + def relation_for_count + raise( + NotImplementedError, + '"relation_for_count" must be implemented and return an ActiveRecord::Relation' + ) + end + + def count + Rails.cache.fetch(cache_key, raw: raw?) { uncached_count }.to_i + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count, raw: raw?) + end + + def uncached_count + relation_for_count.count + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + def raw? + false + end + + def cache_key + raise NotImplementedError, 'cache_key must be implemented and return a String' + end +end diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb index 44da87cb00c..e73c6ad6780 100644 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ b/app/services/ci/fetch_kubernetes_token_service.rb @@ -34,6 +34,7 @@ module Ci kubeclient.get_secrets.as_json rescue KubeException => err raise err unless err.error_code == 404 + [] end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 43b539ded53..997d247be46 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -19,6 +19,7 @@ module Labels # We skipped validations during creation. Let's run them now, after deleting conflicting labels raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid? + new_label end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bc0e7ad4e39..f3b99e1ec8c 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -28,6 +28,7 @@ module MergeRequests def find_target_project return target_project if target_project.present? && can?(current_user, :read_project, target_project) + project.default_merge_request_target end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 1da4dbd9e96..cedfcb50e09 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -10,6 +10,8 @@ module MergeRequests attr_reader :merge_request, :source + delegate :merge_jid, :state, to: :@merge_request + def execute(merge_request) if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService) FfMergeService.new(project, current_user, params).execute(merge_request) @@ -27,6 +29,7 @@ module MergeRequests success end end + log_info("Merge process finished on JID #{merge_jid} with state #{state}") rescue MergeError => e handle_merge_error(log_message: e.message, save_message_on_model: true) end @@ -49,7 +52,9 @@ module MergeRequests def commit message = params[:commit_message] || merge_request.merge_commit_message + log_info("Git merge started on JID #{merge_jid}") commit_id = repository.merge(current_user, source, merge_request, message) + log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}") raise MergeError, 'Conflicts detected during merge' unless commit_id @@ -63,7 +68,9 @@ module MergeRequests end def after_merge + log_info("Post merge started on JID #{merge_jid} with state #{state}") MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) + log_info("Post merge finished on JID #{merge_jid} with state #{state}") if delete_source_branch? DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) @@ -92,6 +99,11 @@ module MergeRequests @merge_request.update(merge_error: log_message) if save_message_on_model end + def log_info(message) + @logger ||= Rails.logger + @logger.info("#{merge_request_info} - #{message}") + end + def merge_request_info merge_request.to_reference(full: true) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index bd9cfd4e0ea..2187f26d1ed 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -6,14 +6,14 @@ module Milestones check_project_milestone!(milestone) Milestone.transaction do - # Destroy all milestones with same title across projects - destroy_old_milestones(milestone) - group_milestone = clone_project_milestone(milestone) move_children_to_group_milestone(group_milestone) - # Just to be safe + # Destroy all milestones with same title across projects + destroy_old_milestones(milestone) + + # Rollback if milestone is not valid unless group_milestone.valid? raise_error(group_milestone.errors.full_messages.to_sentence) end @@ -35,7 +35,7 @@ module Milestones end def move_children_to_group_milestone(group_milestone) - milestone_ids_for_merge(group_milestone).in_groups_of(100) do |milestone_ids| + milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids| update_children(group_milestone, milestone_ids) end end @@ -49,7 +49,12 @@ module Milestones create_service = CreateService.new(group, current_user, params) - create_service.execute + milestone = create_service.execute + + # milestone won't be valid here because of duplicated title + milestone.save(validate: false) + + milestone end def update_children(group_milestone, milestone_ids) @@ -65,12 +70,12 @@ module Milestones @group ||= parent.group || raise_error('Project does not belong to a group.') end - def destroy_old_milestones(group_milestone) - Milestone.where(id: milestone_ids_for_merge(group_milestone)).destroy_all + def destroy_old_milestones(milestone) + Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all end def group_project_ids - @group_project_ids ||= group.projects.map(&:id) + @group_project_ids ||= group.projects.pluck(:id) end def raise_error(message) diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb index aa034315280..7e575b2d6f3 100644 --- a/app/services/projects/count_service.rb +++ b/app/services/projects/count_service.rb @@ -1,7 +1,7 @@ module Projects # Base class for the various service classes that count project data (e.g. # issues or forks). - class CountService + class CountService < BaseCountService # The version of the cache format. This should be bumped whenever the # underlying logic changes. This removes the need for explicitly flushing # all caches. @@ -11,29 +11,6 @@ module Projects @project = project end - def relation_for_count - raise( - NotImplementedError, - '"relation_for_count" must be implemented and return an ActiveRecord::Relation' - ) - end - - def count - Rails.cache.fetch(cache_key) { uncached_count } - end - - def refresh_cache - Rails.cache.write(cache_key, uncached_count) - end - - def uncached_count - relation_for_count.count - end - - def delete_cache - Rails.cache.delete(cache_key) - end - def cache_key_name raise( NotImplementedError, diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb index 3a0fa84b868..d9bdf3a8ad7 100644 --- a/app/services/projects/forks_count_service.rb +++ b/app/services/projects/forks_count_service.rb @@ -1,6 +1,6 @@ module Projects # Service class for getting and caching the number of forks of a project. - class ForksCountService < CountService + class ForksCountService < Projects::CountService def relation_for_count @project.forks end diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb index fbf31214c28..e3a20b4c1e4 100644 --- a/app/services/projects/group_links/destroy_service.rb +++ b/app/services/projects/group_links/destroy_service.rb @@ -3,6 +3,7 @@ module Projects class DestroyService < BaseService def execute(group_link) return false unless group_link + group_link.destroy end end diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb index 3c0d186a73c..25de97325e2 100644 --- a/app/services/projects/open_issues_count_service.rb +++ b/app/services/projects/open_issues_count_service.rb @@ -1,7 +1,7 @@ module Projects # Service class for counting and caching the number of open issues of a # project. - class OpenIssuesCountService < CountService + class OpenIssuesCountService < Projects::CountService def relation_for_count # We don't include confidential issues in this number since this would # expose the number of confidential issues to non project members. diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb index 2a90f78b90d..77e6448fd5e 100644 --- a/app/services/projects/open_merge_requests_count_service.rb +++ b/app/services/projects/open_merge_requests_count_service.rb @@ -1,7 +1,7 @@ module Projects # Service class for counting and caching the number of open merge requests of # a project. - class OpenMergeRequestsCountService < CountService + class OpenMergeRequestsCountService < Projects::CountService def relation_for_count @project.merge_requests.opened end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 5957f612e84..e5cd6fcdfe3 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -60,21 +60,14 @@ module Projects # Notifications project.send_move_instructions(@old_path) - # Move main repository - # TODO: check storage type and NOOP when not using Legacy - unless move_repo_folder(@old_path, @new_path) - raise TransferError.new('Cannot move project') - end - - # Move wiki repo also if present - # TODO: check storage type and NOOP when not using Legacy - move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + # Directories on disk + move_project_folders(project) # Move missing group labels to project Labels::TransferService.new(current_user, @old_group, project).execute # Move uploads - Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) + move_project_uploads(project) # Move pages Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) @@ -131,5 +124,30 @@ module Projects def execute_system_hooks SystemHooksService.new.execute_hooks_for(project, :transfer) end + + def move_project_folders(project) + return if project.hashed_storage?(:repository) + + # Move main repository + unless move_repo_folder(@old_path, @new_path) + raise TransferError.new("Cannot move project") + end + + # Disk path is changed; we need to ensure we reload it + project.reload_repository! + + # Move wiki repo also if present + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") + end + + def move_project_uploads(project) + return if project.hashed_storage?(:attachments) + + Gitlab::UploadsTransfer.new.move_project( + project.path, + @old_namespace.full_path, + @new_namespace.full_path + ) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e946218824c..fe71a405565 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -583,6 +583,10 @@ module SystemNoteService create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) end + def cross_reference?(note_text) + note_text =~ /\A#{cross_reference_note_prefix}/i + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index e694c5761da..575853fd66b 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -208,6 +208,7 @@ class TodoService def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? + todo = Todo.create(attributes.merge(user_id: user.id)) user.update_todos_count_cache todo diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb new file mode 100644 index 00000000000..f82d27eded9 --- /dev/null +++ b/app/services/users/keys_count_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Users + # Service class for getting the number of SSH keys that belong to a user. + class KeysCountService < BaseCountService + attr_reader :user + + # user - The User for which to get the number of SSH keys. + def initialize(user) + @user = user + end + + def relation_for_count + user.keys + end + + def raw? + # Since we're storing simple integers we don't need all of the additional + # Marshal data Rails includes by default. + true + end + + def cache_key + "users/key-count-service/#{user.id}" + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index d4ba3a028be..f4a5cf75018 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -26,11 +26,15 @@ class FileUploader < GitlabUploader # This is used to build Upload paths dynamically based on the model's current # namespace and path, allowing us to ignore renames or transfers. # - # model - Object that responds to `path_with_namespace` + # model - Object that responds to `full_path` and `disk_path` # # Returns a String without a trailing slash - def self.dynamic_path_segment(model) - File.join(CarrierWave.root, base_dir, model.disk_path) + def self.dynamic_path_segment(project) + if project.hashed_storage?(:attachments) + File.join(CarrierWave.root, base_dir, project.disk_path) + else + File.join(CarrierWave.root, base_dir, project.full_path) + end end attr_accessor :model diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb index 098b16017d2..8c7bb750339 100644 --- a/app/validators/certificate_key_validator.rb +++ b/app/validators/certificate_key_validator.rb @@ -17,6 +17,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator def valid_private_key_pem?(value) return false unless value + pkey = OpenSSL::PKey::RSA.new(value) pkey.private? rescue OpenSSL::PKey::PKeyError diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb index e3d18097f71..5239e70a326 100644 --- a/app/validators/certificate_validator.rb +++ b/app/validators/certificate_validator.rb @@ -17,6 +17,7 @@ class CertificateValidator < ActiveModel::EachValidator def valid_certificate_pem?(value) return false unless value + OpenSSL::X509::Certificate.new(value).present? rescue OpenSSL::X509::CertificateError false diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 935787d1a4a..4a2238fe277 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -43,7 +43,7 @@ = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" .hint - Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo + Maximum file size is 1MB. Pages are optimized for a 28px tall header logo .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3a4d5ce0b5c..12658dddc06 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -743,5 +743,56 @@ installations. Set to 0 to completely disable polling. = link_to icon('question-circle'), help_page_path('administration/polling') + %fieldset + %legend User and IP Rate Limits + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_unauthenticated_enabled do + = f.check_box :throttle_unauthenticated_enabled + Enable unauthenticated request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_api_enabled do + = f.check_box :throttle_authenticated_api_enabled + Enable authenticated API request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_web_enabled do + = f.check_box :throttle_authenticated_web_enabled + Enable authenticated web request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index df2bf27be9d..6d8fad0eb8d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -99,7 +99,7 @@ %td.build-link - if project - = link_to ci_status_path(build.pipeline) do + = link_to pipeline_path(build.pipeline) do %strong= build.pipeline.short_sha %td.timestamp diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index cb4fc69d5b8..f5f621507b8 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -4,6 +4,7 @@ = render 'shared/milestones_filter', counts: @milestone_states .nav-controls + = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestones, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index eca7fb9ddb1..d758e314d41 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title "Milestones" - page_title "Milestones" -- header_title group_title(@group, "Milestones", group_milestones_path(@group)) %h3.page-title New Milestone diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index d0c2e0b1d69..021de4f0caf 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -29,7 +29,7 @@ .row.prepend-top-default .col-md-8 - .documentation-index + .documentation-index.wiki = markdown(@help_index) .col-md-4 .panel.panel-default diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1eca412aff9..e2407f6a428 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -7,7 +7,7 @@ = link_to root_path, title: 'Dashboard', id: 'logo' do = brand_header_logo %span.logo-text.hidden-xs - = render 'shared/logo_type.svg' + = brand_header_logo_type - if current_user = render "layouts/nav/dashboard" diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index f8a2ea18989..a9431cc4956 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -10,25 +10,23 @@ .md-area .md-header %ul.nav-links.clearfix - %li.active + %li.md-header-tab.active %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write - %li + %li.md-header-tab %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview - %li.pull-right - .toolbar-group - = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) - = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) - = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) - = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) - = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) - = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) - = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) - .toolbar-group - %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } - = sprite_icon("screen-full") + %li.md-header-toolbar + = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) + = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } + = sprite_icon("screen-full") .md-write-holder = yield diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 2de2cf9e38c..dd473ebe580 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -22,9 +22,11 @@ - diff_files.each do |diff_file| %li %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } - = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") - %span.diff-file-changes-path.append-right-5= diff_file.new_path - .pull-right + = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8") + %span.diff-changed-file-content.append-right-8 + %strong.diff-changed-file-name= diff_file.blob.name + %span.diff-changed-file-path.prepend-top-5= diff_file.new_path + %span.diff-changed-stats %span.cgreen< +#{diff_file.added_lines} %span.cred< diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index d7859c9fbeb..add394a6356 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -19,14 +19,15 @@ .environments-container - if @deployments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any deployments right now. - %p.blank-state-text - Define environments in the deploy stage(s) in - %code .gitlab-ci.yml - to track deployments here. - = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" + .blank-state-row + .blank-state-center + %h2.blank-state-title + You don't have any deployments right now. + %p.blank-state-text + Define environments in the deploy stage(s) in + %code .gitlab-ci.yml + to track deployments here. + = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder .ci-table.environments{ role: 'grid' } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b9fec8af4d7..48410ffee21 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -27,9 +27,9 @@ .issuable-meta - if @issue.confidential - = icon('eye-slash', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon') - if @issue.discussion_locked? - = icon('lock', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@issue, @project, "Issue") .issuable-actions.js-issuable-actions @@ -40,7 +40,7 @@ .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can_update_issue - %li= link_to 'Edit', edit_project_issue_path(@project, @issue) + %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit' - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index b5067367802..17ac8a20a30 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -4,7 +4,7 @@ .sidebar-container .blocks-container .block - %strong.prepend-top-10 + %strong.inline.prepend-top-8 = @build.name - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 2abd2c9e652..1d0aaa47b60 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -57,13 +57,13 @@ .build-trace-container.prepend-top-default .top-bar.js-top-bar - .js-truncated-info.truncated-info.hidden< + .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< Showing last %span.js-truncated-info-size.truncated-info-size>< KiB of log - %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw - .controllers + .controllers.pull-right - if @build.has_trace? = link_to raw_project_job_path(@project, @build), title: 'Show complete raw', diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 72d5c4961ec..75b3db7e505 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -16,7 +16,7 @@ .issuable-meta - if @merge_request.discussion_locked? - = icon('lock', class: 'issuable-warning-icon') + .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@merge_request, @project, "Merge request") .issuable-actions.js-issuable-actions diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index fd3b8c01b83..da364b58e36 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - @sort ||= sort_value_recently_updated -- page_title _('TagsPage|Tags') +- page_title s_('TagsPage|Tags') - add_to_breadcrumbs("Repository", project_tree_path(@project)) .flex-list{ class: container_class } diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index 0a1ccbc5f1c..efa16d38f84 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -2,4 +2,4 @@ = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) .pull-right - %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.commit.authored_date) }).html_safe + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 9ee09262324..969a1677d9a 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -21,7 +21,7 @@ %th= _("Last updated") %th= _("Format") %tbody - - @page.versions.each_with_index do |version, index| + - @page_versions.each_with_index do |version, index| - commit = version %tr %td @@ -37,5 +37,6 @@ %td %strong = version.format += paginate @page_versions, theme: 'gitlab' = render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index de15fc99eda..b3b83cee81a 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -11,8 +11,8 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - = (_("Last edited by %{name}") % { name: "<strong>#{@page.commit.author_name}</strong>" }).html_safe - #{time_ago_with_tooltip(@page.commit.authored_date)} + = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + #{time_ago_with_tooltip(@page.last_version.authored_date)} .nav-controls = render 'main_links' diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 233d8c95eda..736afa085e8 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -11,6 +11,7 @@ %li If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. %li - The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. + The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}. + For repositories that take longer, use a clone/push combination. %li To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}. diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 6356e9f92cb..f4a4bfaec54 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,3 +1,5 @@ +- show_create = local_assigns.fetch(:show_create, false) + - show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project) - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml index 9b989c23cab..333dd1a00b4 100644 --- a/app/views/shared/boards/components/sidebar/_notifications.html.haml +++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml @@ -1,7 +1,5 @@ - if current_user - .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" } - %span.issuable-header-text.hide-collapsed.pull-left - Notifications - %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } - %span - {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}} + .block.subscriptions + %subscriptions{ ":loading" => "issue.isFetching && issue.isFetching.subscriptions", + ":subscribed" => "issue.subscribed", + ":id" => "issue.id" } diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 3dd14466994..311fc187e49 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -104,6 +104,7 @@ class IrkerWorker parents = commit.parents # Return old value if there's no new one return push_data['before'] if parents.empty? + # Or return the first parent-commit parents[0].id end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index 269776a1f62..fdbc049c2df 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -39,6 +39,7 @@ class StuckCiJobsWorker def drop_stuck(status, timeout) search(status, timeout) do |build| return unless build.stuck? + drop_build :stuck, build, status, timeout end end |