diff options
476 files changed, 6602 insertions, 2970 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 65f2bc7045f..d4b375696c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -193,7 +193,7 @@ review-docs-deploy: name: review-docs/$CI_COMMIT_REF_NAME # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 - url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX + url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - ./trigger-build-docs deploy @@ -256,7 +256,7 @@ flaky-examples-check: USE_BUNDLE_INSTALL: "false" NEW_FLAKY_SPECS_REPORT: rspec_flaky/report-new.json stage: post-test - allow_failure: yes + allow_failure: true retry: 0 only: - branches @@ -416,7 +416,6 @@ ee_compat_check: - /^[\d-]+-stable(-ee)?/ - branches@gitlab-org/gitlab-ee - branches@gitlab/gitlab-ee - allow_failure: no retry: 0 artifacts: name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" @@ -475,7 +474,7 @@ migration:path-mysql: <<: *pull-cache stage: test script: - - bundle exec rake db:rollback STEP=120 + - bundle exec rake db:rollback STEP=119 - bundle exec rake db:migrate db:rollback-pg: @@ -578,7 +577,7 @@ codequality: script: - cp .rubocop.yml .rubocop.yml.bak - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - mv .rubocop.yml.bak .rubocop.yml artifacts: diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md index 1278061a410..5b55eb1374b 100644 --- a/.gitlab/issue_templates/Feature Proposal.md +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -1,22 +1,3 @@ -Please read this! - -Before opening a new issue, make sure to search for keywords in the issues -filtered by the "feature proposal" label: - -For the Community Edition issue tracker: - -- https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal - -For the Enterprise Edition issue tracker: - -- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal - -and verify the issue you're about to submit isn't a duplicate. - -Please remove this notice if you're confident your issue isn't a duplicate. - ------- - ### Description (Include problem, use cases, benefits, and/or goals) @@ -25,26 +6,4 @@ Please remove this notice if you're confident your issue isn't a duplicate. ### Links / references -### Documentation blurb - -#### Overview - -What is it? -Why should someone use this feature? -What is the underlying (business) problem? -How do you use this feature? - -#### Use cases - -Who is this for? Provide one or more use cases. - -### Feature checklist - -Make sure these are completed before closing the issue, -with a link to the relevant commit. - -- [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance) -- [ ] Documentation -- [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml) - -/label ~"feature proposal"
\ No newline at end of file +/label ~"feature proposal" diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7f422a161ae..524456c7767 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.53.0 +0.54.0 @@ -263,6 +263,8 @@ gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development +gem 'batch-loader' + # Perf bar gem 'peek', '~> 1.0.1' gem 'peek-gc', '~> 0.0.2' @@ -343,7 +345,7 @@ group :development, :test do gem 'benchmark-ips', '~> 2.3.0', require: false - gem 'license_finder', '~> 2.1.0', require: false + gem 'license_finder', '~> 3.1', require: false gem 'knapsack', '~> 1.11.0' gem 'activerecord_sane_schema_dumper', '0.2' @@ -398,7 +400,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.54.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 43555a01037..4787be92365 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) + batch-loader (1.1.1) bcrypt (3.1.11) bcrypt_pbkdf (1.0.0) benchmark-ips (2.3.0) @@ -83,6 +84,7 @@ GEM bindata (2.4.1) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + blankslate (2.1.2.4) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) @@ -274,7 +276,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.52.0) + gitaly-proto (0.54.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -354,10 +356,10 @@ GEM rake grape_logging (1.7.0) grape - grpc (1.6.6) + grpc (1.7.2) google-protobuf (~> 3.1) googleapis-common-protos-types (~> 1.0.0) - googleauth (~> 0.5.1) + googleauth (>= 0.5.1, < 0.7) haml (4.0.7) tilt haml_lint (0.26.0) @@ -451,11 +453,13 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) - license_finder (2.1.0) + license_finder (3.1.1) bundler httparty rubyzip thor + toml (= 0.1.2) + with_env (> 1.0) xml-simple licensee (8.7.0) rugged (~> 0.24) @@ -571,6 +575,8 @@ GEM activerecord (>= 4.0, < 5.2) parser (2.4.0.0) ast (~> 2.2) + parslet (1.5.0) + blankslate (~> 2.0) path_expander (1.0.1) peek (1.0.1) concurrent-ruby (>= 0.9.0) @@ -898,6 +904,8 @@ GEM tilt (2.0.6) timecop (0.8.1) timfel-krb5-auth (0.8.3) + toml (0.1.2) + parslet (~> 1.5.0) toml-rb (0.3.15) citrus (~> 3.0, > 3.0) truncato (0.7.10) @@ -952,6 +960,7 @@ GEM builder expression_parser rinku + with_env (1.1.0) xml-simple (1.1.5) xpath (2.1.0) nokogiri (~> 1.3) @@ -974,6 +983,7 @@ DEPENDENCIES awesome_print (~> 1.2.0) babosa (~> 1.0.2) base32 (~> 0.3.0) + batch-loader bcrypt_pbkdf (~> 1.0) benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) @@ -1026,7 +1036,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.52.0) + gitaly-proto (~> 0.54.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1058,7 +1068,7 @@ DEPENDENCIES knapsack (~> 1.11.0) kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) - license_finder (~> 2.1.0) + license_finder (~> 3.1) licensee (~> 8.7.0) lograge (~> 0.5) loofah (~> 2.0.3) diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png Binary files differindex 5dcd9c09b70..723c2c3f4c8 100644 --- a/app/assets/images/emoji.png +++ b/app/assets/images/emoji.png diff --git a/app/assets/images/emoji/gay_pride_flag.png b/app/assets/images/emoji/gay_pride_flag.png Binary files differnew file mode 100644 index 00000000000..1bec5f2ffd7 --- /dev/null +++ b/app/assets/images/emoji/gay_pride_flag.png diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png Binary files differindex 078f0657f95..9cf2458df1a 100644 --- a/app/assets/images/emoji/mrs_claus.png +++ b/app/assets/images/emoji/mrs_claus.png diff --git a/app/assets/images/emoji/speech_left.png b/app/assets/images/emoji/speech_left.png Binary files differnew file mode 100644 index 00000000000..00c05959bcd --- /dev/null +++ b/app/assets/images/emoji/speech_left.png diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png Binary files differindex b0fa9e1139e..987279c13cc 100644 --- a/app/assets/images/emoji@2x.png +++ b/app/assets/images/emoji@2x.png 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..b4307761c6b 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(); @@ -339,7 +339,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 +485,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 +521,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 +624,6 @@ import Diff from './diff'; case 'show': new Star(); new ProjectNew(); - new ProjectShow(); new NotificationsDropdown(); break; case 'wikis': diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index 3fd23efa9f8..e9defb62cf8 100644 --- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) { return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; } +// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters +const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16) +const rainbowCodePoint = 127752; // parseInt('1F308', 16) +function isRainbowFlagEmoji(emojiUnicode) { + const characters = Array.from(emojiUnicode); + // Length 4 because flags are made of 2 characters which are surrogate pairs + return emojiUnicode.length === 4 && + characters[0].codePointAt(0) === baseFlagCodePoint && + characters[1].codePointAt(0) === rainbowCodePoint; +} + // Chrome <57 renders keycaps oddly // See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 // Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png @@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) { // in `isEmojiUnicodeSupported` logic function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { const isFlagResult = isFlagEmoji(emojiUnicode); + const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode); return ( (unicodeSupportMap.flag && isFlagResult) || - !isFlagResult + (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) || + (!isFlagResult && !isRainbowFlagResult) ); } @@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe export { isEmojiUnicodeSupported as default, isFlagEmoji, + isRainbowFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji, diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 755381c2f95..c18d07dad43 100644 --- a/app/assets/javascripts/emoji/support/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -1,5 +1,7 @@ import AccessorUtilities from '../../lib/utils/accessor'; +const GL_EMOJI_VERSION = '0.2.0'; + const unicodeSupportTestMap = { // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ // occupationZwj: '\u{1F468}\u{200D}\u{1F393}', @@ -13,6 +15,7 @@ const unicodeSupportTestMap = { horseRacing: '\u{1F3C7}\u{1F3FF}', // US flag, http://emojipedia.org/flags/ flag: '\u{1F1FA}\u{1F1F8}', + rainbowFlag: '\u{1F3F3}\u{1F308}', // http://emojipedia.org/modifiers/ skinToneModifier: [ // spy_tone5 @@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) { } export default function getUnicodeSupportMap() { - let unicodeSupportMap; - let userAgentFromCache; - const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); - if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + let glEmojiVersionFromCache; + let userAgentFromCache; + if (isLocalStorageAvailable) { + glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version'); + userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + } + let unicodeSupportMap; try { unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); } catch (err) { // swallow } - if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { + if ( + !unicodeSupportMap || + glEmojiVersionFromCache !== GL_EMOJI_VERSION || + userAgentFromCache !== navigator.userAgent + ) { unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); if (isLocalStorageAvailable) { + window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); } 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 d1aa83ea57f..4e39d483b31 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -29,6 +29,11 @@ export default { required: false, default: false, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, issuableRef: { type: String, required: true, @@ -92,6 +97,16 @@ export default { type: String, required: true, }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, data() { const store = new Store({ @@ -157,21 +172,21 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - window.Flash('Error updating issue'); + window.Flash(`Error updating ${this.issuableType}`); }); }, deleteIssuable() { this.service.deleteIssuable() .then(res => res.json()) .then((data) => { - // Stop the poll so we don't get 404's with the issue not existing + // Stop the poll so we don't get 404's with the issuable not existing this.poll.stop(); gl.utils.visitUrl(data.web_url); }) .catch(() => { eventHub.$emit('close.form'); - window.Flash('Error deleting issue'); + window.Flash(`Error deleting ${this.issuableType}`); }); }, }, @@ -223,6 +238,8 @@ export default { :markdown-preview-path="markdownPreviewPath" :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/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 8c81575fe6f..a539506bce2 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -13,6 +13,11 @@ type: Object, required: true, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -23,6 +28,9 @@ isSubmitEnabled() { return this.formState.title.trim() !== ''; }, + shouldShowDeleteButton() { + return this.canDestroy && this.showDeleteButton; + }, }, methods: { closeForm() { @@ -62,7 +70,7 @@ Cancel </button> <button - v-if="canDestroy" + v-if="shouldShowDeleteButton" class="btn btn-danger pull-right append-right-default" :class="{ disabled: deleteLoading }" type="button" 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 28bf6c67ea5..d61776d480d 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -36,6 +36,16 @@ type: String, required: true, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, components: { lockedWarning, @@ -78,9 +88,11 @@ <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" /> + :can-destroy="canDestroy" + :show-delete-button="showDeleteButton" /> </form> </template> 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..b7ef1ecd923 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,11 +66,6 @@ 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'; @@ -84,9 +74,6 @@ 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/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/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 0cc2653761c..247943f83e6 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -35,6 +35,11 @@ export default { type: String, required: false, }, + containerClass: { + type: String, + required: false, + default: 'btn btn-align-content', + }, }, components: { loadingIcon, @@ -49,9 +54,9 @@ export default { <template> <button - class="btn btn-align-content" @click="onClick" type="button" + :class="containerClass" :disabled="loading || disabled" > <transition name="fade"> 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/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 9c1439dfad5..91976ca1f56 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -353,3 +353,7 @@ display: -webkit-flex; display: flex; } + +.flex-right { + margin-left: auto; +} 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/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss index 925415f84b1..0174e17b660 100644 --- a/app/assets/stylesheets/framework/emoji-sprites.scss +++ b/app/assets/stylesheets/framework/emoji-sprites.scss @@ -765,1031 +765,1033 @@ .emoji-full_moon { background-position: -160px -540px; } .emoji-full_moon_with_face { background-position: -180px -540px; } .emoji-game_die { background-position: -200px -540px; } -.emoji-gear { background-position: -220px -540px; } -.emoji-gem { background-position: -240px -540px; } -.emoji-gemini { background-position: -260px -540px; } -.emoji-ghost { background-position: -280px -540px; } -.emoji-gift { background-position: -300px -540px; } -.emoji-gift_heart { background-position: -320px -540px; } -.emoji-girl { background-position: -340px -540px; } -.emoji-girl_tone1 { background-position: -360px -540px; } -.emoji-girl_tone2 { background-position: -380px -540px; } -.emoji-girl_tone3 { background-position: -400px -540px; } -.emoji-girl_tone4 { background-position: -420px -540px; } -.emoji-girl_tone5 { background-position: -440px -540px; } -.emoji-globe_with_meridians { background-position: -460px -540px; } -.emoji-goal { background-position: -480px -540px; } -.emoji-goat { background-position: -500px -540px; } -.emoji-golf { background-position: -520px -540px; } -.emoji-golfer { background-position: -540px -540px; } -.emoji-gorilla { background-position: -560px 0; } -.emoji-grapes { background-position: -560px -20px; } -.emoji-green_apple { background-position: -560px -40px; } -.emoji-green_book { background-position: -560px -60px; } -.emoji-green_heart { background-position: -560px -80px; } -.emoji-grey_exclamation { background-position: -560px -100px; } -.emoji-grey_question { background-position: -560px -120px; } -.emoji-grimacing { background-position: -560px -140px; } -.emoji-grin { background-position: -560px -160px; } -.emoji-grinning { background-position: -560px -180px; } -.emoji-guardsman { background-position: -560px -200px; } -.emoji-guardsman_tone1 { background-position: -560px -220px; } -.emoji-guardsman_tone2 { background-position: -560px -240px; } -.emoji-guardsman_tone3 { background-position: -560px -260px; } -.emoji-guardsman_tone4 { background-position: -560px -280px; } -.emoji-guardsman_tone5 { background-position: -560px -300px; } -.emoji-guitar { background-position: -560px -320px; } -.emoji-gun { background-position: -560px -340px; } -.emoji-haircut { background-position: -560px -360px; } -.emoji-haircut_tone1 { background-position: -560px -380px; } -.emoji-haircut_tone2 { background-position: -560px -400px; } -.emoji-haircut_tone3 { background-position: -560px -420px; } -.emoji-haircut_tone4 { background-position: -560px -440px; } -.emoji-haircut_tone5 { background-position: -560px -460px; } -.emoji-hamburger { background-position: -560px -480px; } -.emoji-hammer { background-position: -560px -500px; } -.emoji-hammer_pick { background-position: -560px -520px; } -.emoji-hamster { background-position: -560px -540px; } -.emoji-hand_splayed { background-position: 0 -560px; } -.emoji-hand_splayed_tone1 { background-position: -20px -560px; } -.emoji-hand_splayed_tone2 { background-position: -40px -560px; } -.emoji-hand_splayed_tone3 { background-position: -60px -560px; } -.emoji-hand_splayed_tone4 { background-position: -80px -560px; } -.emoji-hand_splayed_tone5 { background-position: -100px -560px; } -.emoji-handbag { background-position: -120px -560px; } -.emoji-handball { background-position: -140px -560px; } -.emoji-handball_tone1 { background-position: -160px -560px; } -.emoji-handball_tone2 { background-position: -180px -560px; } -.emoji-handball_tone3 { background-position: -200px -560px; } -.emoji-handball_tone4 { background-position: -220px -560px; } -.emoji-handball_tone5 { background-position: -240px -560px; } -.emoji-handshake { background-position: -260px -560px; } -.emoji-handshake_tone1 { background-position: -280px -560px; } -.emoji-handshake_tone2 { background-position: -300px -560px; } -.emoji-handshake_tone3 { background-position: -320px -560px; } -.emoji-handshake_tone4 { background-position: -340px -560px; } -.emoji-handshake_tone5 { background-position: -360px -560px; } -.emoji-hash { background-position: -380px -560px; } -.emoji-hatched_chick { background-position: -400px -560px; } -.emoji-hatching_chick { background-position: -420px -560px; } -.emoji-head_bandage { background-position: -440px -560px; } -.emoji-headphones { background-position: -460px -560px; } -.emoji-hear_no_evil { background-position: -480px -560px; } -.emoji-heart { background-position: -500px -560px; } -.emoji-heart_decoration { background-position: -520px -560px; } -.emoji-heart_exclamation { background-position: -540px -560px; } -.emoji-heart_eyes { background-position: -560px -560px; } -.emoji-heart_eyes_cat { background-position: -580px 0; } -.emoji-heartbeat { background-position: -580px -20px; } -.emoji-heartpulse { background-position: -580px -40px; } -.emoji-hearts { background-position: -580px -60px; } -.emoji-heavy_check_mark { background-position: -580px -80px; } -.emoji-heavy_division_sign { background-position: -580px -100px; } -.emoji-heavy_dollar_sign { background-position: -580px -120px; } -.emoji-heavy_minus_sign { background-position: -580px -140px; } -.emoji-heavy_multiplication_x { background-position: -580px -160px; } -.emoji-heavy_plus_sign { background-position: -580px -180px; } -.emoji-helicopter { background-position: -580px -200px; } -.emoji-helmet_with_cross { background-position: -580px -220px; } -.emoji-herb { background-position: -580px -240px; } -.emoji-hibiscus { background-position: -580px -260px; } -.emoji-high_brightness { background-position: -580px -280px; } -.emoji-high_heel { background-position: -580px -300px; } -.emoji-hockey { background-position: -580px -320px; } -.emoji-hole { background-position: -580px -340px; } -.emoji-homes { background-position: -580px -360px; } -.emoji-honey_pot { background-position: -580px -380px; } -.emoji-horse { background-position: -580px -400px; } -.emoji-horse_racing { background-position: -580px -420px; } -.emoji-horse_racing_tone1 { background-position: -580px -440px; } -.emoji-horse_racing_tone2 { background-position: -580px -460px; } -.emoji-horse_racing_tone3 { background-position: -580px -480px; } -.emoji-horse_racing_tone4 { background-position: -580px -500px; } -.emoji-horse_racing_tone5 { background-position: -580px -520px; } -.emoji-hospital { background-position: -580px -540px; } -.emoji-hot_pepper { background-position: -580px -560px; } -.emoji-hotdog { background-position: 0 -580px; } -.emoji-hotel { background-position: -20px -580px; } -.emoji-hotsprings { background-position: -40px -580px; } -.emoji-hourglass { background-position: -60px -580px; } -.emoji-hourglass_flowing_sand { background-position: -80px -580px; } -.emoji-house { background-position: -100px -580px; } -.emoji-house_abandoned { background-position: -120px -580px; } -.emoji-house_with_garden { background-position: -140px -580px; } -.emoji-hugging { background-position: -160px -580px; } -.emoji-hushed { background-position: -180px -580px; } -.emoji-ice_cream { background-position: -200px -580px; } -.emoji-ice_skate { background-position: -220px -580px; } -.emoji-icecream { background-position: -240px -580px; } -.emoji-id { background-position: -260px -580px; } -.emoji-ideograph_advantage { background-position: -280px -580px; } -.emoji-imp { background-position: -300px -580px; } -.emoji-inbox_tray { background-position: -320px -580px; } -.emoji-incoming_envelope { background-position: -340px -580px; } -.emoji-information_desk_person { background-position: -360px -580px; } -.emoji-information_desk_person_tone1 { background-position: -380px -580px; } -.emoji-information_desk_person_tone2 { background-position: -400px -580px; } -.emoji-information_desk_person_tone3 { background-position: -420px -580px; } -.emoji-information_desk_person_tone4 { background-position: -440px -580px; } -.emoji-information_desk_person_tone5 { background-position: -460px -580px; } -.emoji-information_source { background-position: -480px -580px; } -.emoji-innocent { background-position: -500px -580px; } -.emoji-interrobang { background-position: -520px -580px; } -.emoji-iphone { background-position: -540px -580px; } -.emoji-island { background-position: -560px -580px; } -.emoji-izakaya_lantern { background-position: -580px -580px; } -.emoji-jack_o_lantern { background-position: -600px 0; } -.emoji-japan { background-position: -600px -20px; } -.emoji-japanese_castle { background-position: -600px -40px; } -.emoji-japanese_goblin { background-position: -600px -60px; } -.emoji-japanese_ogre { background-position: -600px -80px; } -.emoji-jeans { background-position: -600px -100px; } -.emoji-joy { background-position: -600px -120px; } -.emoji-joy_cat { background-position: -600px -140px; } -.emoji-joystick { background-position: -600px -160px; } -.emoji-juggling { background-position: -600px -180px; } -.emoji-juggling_tone1 { background-position: -600px -200px; } -.emoji-juggling_tone2 { background-position: -600px -220px; } -.emoji-juggling_tone3 { background-position: -600px -240px; } -.emoji-juggling_tone4 { background-position: -600px -260px; } -.emoji-juggling_tone5 { background-position: -600px -280px; } -.emoji-kaaba { background-position: -600px -300px; } -.emoji-key { background-position: -600px -320px; } -.emoji-key2 { background-position: -600px -340px; } -.emoji-keyboard { background-position: -600px -360px; } -.emoji-kimono { background-position: -600px -380px; } -.emoji-kiss { background-position: -600px -400px; } -.emoji-kiss_mm { background-position: -600px -420px; } -.emoji-kiss_ww { background-position: -600px -440px; } -.emoji-kissing { background-position: -600px -460px; } -.emoji-kissing_cat { background-position: -600px -480px; } -.emoji-kissing_closed_eyes { background-position: -600px -500px; } -.emoji-kissing_heart { background-position: -600px -520px; } -.emoji-kissing_smiling_eyes { background-position: -600px -540px; } -.emoji-kiwi { background-position: -600px -560px; } -.emoji-knife { background-position: -600px -580px; } -.emoji-koala { background-position: 0 -600px; } -.emoji-koko { background-position: -20px -600px; } -.emoji-label { background-position: -40px -600px; } -.emoji-large_blue_circle { background-position: -60px -600px; } -.emoji-large_blue_diamond { background-position: -80px -600px; } -.emoji-large_orange_diamond { background-position: -100px -600px; } -.emoji-last_quarter_moon { background-position: -120px -600px; } -.emoji-last_quarter_moon_with_face { background-position: -140px -600px; } -.emoji-laughing { background-position: -160px -600px; } -.emoji-leaves { background-position: -180px -600px; } -.emoji-ledger { background-position: -200px -600px; } -.emoji-left_facing_fist { background-position: -220px -600px; } -.emoji-left_facing_fist_tone1 { background-position: -240px -600px; } -.emoji-left_facing_fist_tone2 { background-position: -260px -600px; } -.emoji-left_facing_fist_tone3 { background-position: -280px -600px; } -.emoji-left_facing_fist_tone4 { background-position: -300px -600px; } -.emoji-left_facing_fist_tone5 { background-position: -320px -600px; } -.emoji-left_luggage { background-position: -340px -600px; } -.emoji-left_right_arrow { background-position: -360px -600px; } -.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; } -.emoji-lemon { background-position: -400px -600px; } -.emoji-leo { background-position: -420px -600px; } -.emoji-leopard { background-position: -440px -600px; } -.emoji-level_slider { background-position: -460px -600px; } -.emoji-levitate { background-position: -480px -600px; } -.emoji-libra { background-position: -500px -600px; } -.emoji-lifter { background-position: -520px -600px; } -.emoji-lifter_tone1 { background-position: -540px -600px; } -.emoji-lifter_tone2 { background-position: -560px -600px; } -.emoji-lifter_tone3 { background-position: -580px -600px; } -.emoji-lifter_tone4 { background-position: -600px -600px; } -.emoji-lifter_tone5 { background-position: -620px 0; } -.emoji-light_rail { background-position: -620px -20px; } -.emoji-link { background-position: -620px -40px; } -.emoji-lion_face { background-position: -620px -60px; } -.emoji-lips { background-position: -620px -80px; } -.emoji-lipstick { background-position: -620px -100px; } -.emoji-lizard { background-position: -620px -120px; } -.emoji-lock { background-position: -620px -140px; } -.emoji-lock_with_ink_pen { background-position: -620px -160px; } -.emoji-lollipop { background-position: -620px -180px; } -.emoji-loop { background-position: -620px -200px; } -.emoji-loud_sound { background-position: -620px -220px; } -.emoji-loudspeaker { background-position: -620px -240px; } -.emoji-love_hotel { background-position: -620px -260px; } -.emoji-love_letter { background-position: -620px -280px; } -.emoji-low_brightness { background-position: -620px -300px; } -.emoji-lying_face { background-position: -620px -320px; } -.emoji-m { background-position: -620px -340px; } -.emoji-mag { background-position: -620px -360px; } -.emoji-mag_right { background-position: -620px -380px; } -.emoji-mahjong { background-position: -620px -400px; } -.emoji-mailbox { background-position: -620px -420px; } -.emoji-mailbox_closed { background-position: -620px -440px; } -.emoji-mailbox_with_mail { background-position: -620px -460px; } -.emoji-mailbox_with_no_mail { background-position: -620px -480px; } -.emoji-man { background-position: -620px -500px; } -.emoji-man_dancing { background-position: -620px -520px; } -.emoji-man_dancing_tone1 { background-position: -620px -540px; } -.emoji-man_dancing_tone2 { background-position: -620px -560px; } -.emoji-man_dancing_tone3 { background-position: -620px -580px; } -.emoji-man_dancing_tone4 { background-position: -620px -600px; } -.emoji-man_dancing_tone5 { background-position: 0 -620px; } -.emoji-man_in_tuxedo { background-position: -20px -620px; } -.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; } -.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; } -.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; } -.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; } -.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; } -.emoji-man_tone1 { background-position: -140px -620px; } -.emoji-man_tone2 { background-position: -160px -620px; } -.emoji-man_tone3 { background-position: -180px -620px; } -.emoji-man_tone4 { background-position: -200px -620px; } -.emoji-man_tone5 { background-position: -220px -620px; } -.emoji-man_with_gua_pi_mao { background-position: -240px -620px; } -.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; } -.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; } -.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; } -.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; } -.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; } -.emoji-man_with_turban { background-position: -360px -620px; } -.emoji-man_with_turban_tone1 { background-position: -380px -620px; } -.emoji-man_with_turban_tone2 { background-position: -400px -620px; } -.emoji-man_with_turban_tone3 { background-position: -420px -620px; } -.emoji-man_with_turban_tone4 { background-position: -440px -620px; } -.emoji-man_with_turban_tone5 { background-position: -460px -620px; } -.emoji-mans_shoe { background-position: -480px -620px; } -.emoji-map { background-position: -500px -620px; } -.emoji-maple_leaf { background-position: -520px -620px; } -.emoji-martial_arts_uniform { background-position: -540px -620px; } -.emoji-mask { background-position: -560px -620px; } -.emoji-massage { background-position: -580px -620px; } -.emoji-massage_tone1 { background-position: -600px -620px; } -.emoji-massage_tone2 { background-position: -620px -620px; } -.emoji-massage_tone3 { background-position: -640px 0; } -.emoji-massage_tone4 { background-position: -640px -20px; } -.emoji-massage_tone5 { background-position: -640px -40px; } -.emoji-meat_on_bone { background-position: -640px -60px; } -.emoji-medal { background-position: -640px -80px; } -.emoji-mega { background-position: -640px -100px; } -.emoji-melon { background-position: -640px -120px; } -.emoji-menorah { background-position: -640px -140px; } -.emoji-mens { background-position: -640px -160px; } -.emoji-metal { background-position: -640px -180px; } -.emoji-metal_tone1 { background-position: -640px -200px; } -.emoji-metal_tone2 { background-position: -640px -220px; } -.emoji-metal_tone3 { background-position: -640px -240px; } -.emoji-metal_tone4 { background-position: -640px -260px; } -.emoji-metal_tone5 { background-position: -640px -280px; } -.emoji-metro { background-position: -640px -300px; } -.emoji-microphone { background-position: -640px -320px; } -.emoji-microphone2 { background-position: -640px -340px; } -.emoji-microscope { background-position: -640px -360px; } -.emoji-middle_finger { background-position: -640px -380px; } -.emoji-middle_finger_tone1 { background-position: -640px -400px; } -.emoji-middle_finger_tone2 { background-position: -640px -420px; } -.emoji-middle_finger_tone3 { background-position: -640px -440px; } -.emoji-middle_finger_tone4 { background-position: -640px -460px; } -.emoji-middle_finger_tone5 { background-position: -640px -480px; } -.emoji-military_medal { background-position: -640px -500px; } -.emoji-milk { background-position: -640px -520px; } -.emoji-milky_way { background-position: -640px -540px; } -.emoji-minibus { background-position: -640px -560px; } -.emoji-minidisc { background-position: -640px -580px; } -.emoji-mobile_phone_off { background-position: -640px -600px; } -.emoji-money_mouth { background-position: -640px -620px; } -.emoji-money_with_wings { background-position: 0 -640px; } -.emoji-moneybag { background-position: -20px -640px; } -.emoji-monkey { background-position: -40px -640px; } -.emoji-monkey_face { background-position: -60px -640px; } -.emoji-monorail { background-position: -80px -640px; } -.emoji-mortar_board { background-position: -100px -640px; } -.emoji-mosque { background-position: -120px -640px; } -.emoji-motor_scooter { background-position: -140px -640px; } -.emoji-motorboat { background-position: -160px -640px; } -.emoji-motorcycle { background-position: -180px -640px; } -.emoji-motorway { background-position: -200px -640px; } -.emoji-mount_fuji { background-position: -220px -640px; } -.emoji-mountain { background-position: -240px -640px; } -.emoji-mountain_bicyclist { background-position: -260px -640px; } -.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; } -.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; } -.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; } -.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; } -.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; } -.emoji-mountain_cableway { background-position: -380px -640px; } -.emoji-mountain_railway { background-position: -400px -640px; } -.emoji-mountain_snow { background-position: -420px -640px; } -.emoji-mouse { background-position: -440px -640px; } -.emoji-mouse2 { background-position: -460px -640px; } -.emoji-mouse_three_button { background-position: -480px -640px; } -.emoji-movie_camera { background-position: -500px -640px; } -.emoji-moyai { background-position: -520px -640px; } -.emoji-mrs_claus { background-position: -540px -640px; } -.emoji-mrs_claus_tone1 { background-position: -560px -640px; } -.emoji-mrs_claus_tone2 { background-position: -580px -640px; } -.emoji-mrs_claus_tone3 { background-position: -600px -640px; } -.emoji-mrs_claus_tone4 { background-position: -620px -640px; } -.emoji-mrs_claus_tone5 { background-position: -640px -640px; } -.emoji-muscle { background-position: -660px 0; } -.emoji-muscle_tone1 { background-position: -660px -20px; } -.emoji-muscle_tone2 { background-position: -660px -40px; } -.emoji-muscle_tone3 { background-position: -660px -60px; } -.emoji-muscle_tone4 { background-position: -660px -80px; } -.emoji-muscle_tone5 { background-position: -660px -100px; } -.emoji-mushroom { background-position: -660px -120px; } -.emoji-musical_keyboard { background-position: -660px -140px; } -.emoji-musical_note { background-position: -660px -160px; } -.emoji-musical_score { background-position: -660px -180px; } -.emoji-mute { background-position: -660px -200px; } -.emoji-nail_care { background-position: -660px -220px; } -.emoji-nail_care_tone1 { background-position: -660px -240px; } -.emoji-nail_care_tone2 { background-position: -660px -260px; } -.emoji-nail_care_tone3 { background-position: -660px -280px; } -.emoji-nail_care_tone4 { background-position: -660px -300px; } -.emoji-nail_care_tone5 { background-position: -660px -320px; } -.emoji-name_badge { background-position: -660px -340px; } -.emoji-nauseated_face { background-position: -660px -360px; } -.emoji-necktie { background-position: -660px -380px; } -.emoji-negative_squared_cross_mark { background-position: -660px -400px; } -.emoji-nerd { background-position: -660px -420px; } -.emoji-neutral_face { background-position: -660px -440px; } -.emoji-new { background-position: -660px -460px; } -.emoji-new_moon { background-position: -660px -480px; } -.emoji-new_moon_with_face { background-position: -660px -500px; } -.emoji-newspaper { background-position: -660px -520px; } -.emoji-newspaper2 { background-position: -660px -540px; } -.emoji-ng { background-position: -660px -560px; } -.emoji-night_with_stars { background-position: -660px -580px; } -.emoji-nine { background-position: -660px -600px; } -.emoji-no_bell { background-position: -660px -620px; } -.emoji-no_bicycles { background-position: -660px -640px; } -.emoji-no_entry { background-position: 0 -660px; } -.emoji-no_entry_sign { background-position: -20px -660px; } -.emoji-no_good { background-position: -40px -660px; } -.emoji-no_good_tone1 { background-position: -60px -660px; } -.emoji-no_good_tone2 { background-position: -80px -660px; } -.emoji-no_good_tone3 { background-position: -100px -660px; } -.emoji-no_good_tone4 { background-position: -120px -660px; } -.emoji-no_good_tone5 { background-position: -140px -660px; } -.emoji-no_mobile_phones { background-position: -160px -660px; } -.emoji-no_mouth { background-position: -180px -660px; } -.emoji-no_pedestrians { background-position: -200px -660px; } -.emoji-no_smoking { background-position: -220px -660px; } -.emoji-non-potable_water { background-position: -240px -660px; } -.emoji-nose { background-position: -260px -660px; } -.emoji-nose_tone1 { background-position: -280px -660px; } -.emoji-nose_tone2 { background-position: -300px -660px; } -.emoji-nose_tone3 { background-position: -320px -660px; } -.emoji-nose_tone4 { background-position: -340px -660px; } -.emoji-nose_tone5 { background-position: -360px -660px; } -.emoji-notebook { background-position: -380px -660px; } -.emoji-notebook_with_decorative_cover { background-position: -400px -660px; } -.emoji-notepad_spiral { background-position: -420px -660px; } -.emoji-notes { background-position: -440px -660px; } -.emoji-nut_and_bolt { background-position: -460px -660px; } -.emoji-o { background-position: -480px -660px; } -.emoji-o2 { background-position: -500px -660px; } -.emoji-ocean { background-position: -520px -660px; } -.emoji-octagonal_sign { background-position: -540px -660px; } -.emoji-octopus { background-position: -560px -660px; } -.emoji-oden { background-position: -580px -660px; } -.emoji-office { background-position: -600px -660px; } -.emoji-oil { background-position: -620px -660px; } -.emoji-ok { background-position: -640px -660px; } -.emoji-ok_hand { background-position: -660px -660px; } -.emoji-ok_hand_tone1 { background-position: -680px 0; } -.emoji-ok_hand_tone2 { background-position: -680px -20px; } -.emoji-ok_hand_tone3 { background-position: -680px -40px; } -.emoji-ok_hand_tone4 { background-position: -680px -60px; } -.emoji-ok_hand_tone5 { background-position: -680px -80px; } -.emoji-ok_woman { background-position: -680px -100px; } -.emoji-ok_woman_tone1 { background-position: -680px -120px; } -.emoji-ok_woman_tone2 { background-position: -680px -140px; } -.emoji-ok_woman_tone3 { background-position: -680px -160px; } -.emoji-ok_woman_tone4 { background-position: -680px -180px; } -.emoji-ok_woman_tone5 { background-position: -680px -200px; } -.emoji-older_man { background-position: -680px -220px; } -.emoji-older_man_tone1 { background-position: -680px -240px; } -.emoji-older_man_tone2 { background-position: -680px -260px; } -.emoji-older_man_tone3 { background-position: -680px -280px; } -.emoji-older_man_tone4 { background-position: -680px -300px; } -.emoji-older_man_tone5 { background-position: -680px -320px; } -.emoji-older_woman { background-position: -680px -340px; } -.emoji-older_woman_tone1 { background-position: -680px -360px; } -.emoji-older_woman_tone2 { background-position: -680px -380px; } -.emoji-older_woman_tone3 { background-position: -680px -400px; } -.emoji-older_woman_tone4 { background-position: -680px -420px; } -.emoji-older_woman_tone5 { background-position: -680px -440px; } -.emoji-om_symbol { background-position: -680px -460px; } -.emoji-on { background-position: -680px -480px; } -.emoji-oncoming_automobile { background-position: -680px -500px; } -.emoji-oncoming_bus { background-position: -680px -520px; } -.emoji-oncoming_police_car { background-position: -680px -540px; } -.emoji-oncoming_taxi { background-position: -680px -560px; } -.emoji-one { background-position: -680px -580px; } -.emoji-open_file_folder { background-position: -680px -600px; } -.emoji-open_hands { background-position: -680px -620px; } -.emoji-open_hands_tone1 { background-position: -680px -640px; } -.emoji-open_hands_tone2 { background-position: -680px -660px; } -.emoji-open_hands_tone3 { background-position: 0 -680px; } -.emoji-open_hands_tone4 { background-position: -20px -680px; } -.emoji-open_hands_tone5 { background-position: -40px -680px; } -.emoji-open_mouth { background-position: -60px -680px; } -.emoji-ophiuchus { background-position: -80px -680px; } -.emoji-orange_book { background-position: -100px -680px; } -.emoji-orthodox_cross { background-position: -120px -680px; } -.emoji-outbox_tray { background-position: -140px -680px; } -.emoji-owl { background-position: -160px -680px; } -.emoji-ox { background-position: -180px -680px; } -.emoji-package { background-position: -200px -680px; } -.emoji-page_facing_up { background-position: -220px -680px; } -.emoji-page_with_curl { background-position: -240px -680px; } -.emoji-pager { background-position: -260px -680px; } -.emoji-paintbrush { background-position: -280px -680px; } -.emoji-palm_tree { background-position: -300px -680px; } -.emoji-pancakes { background-position: -320px -680px; } -.emoji-panda_face { background-position: -340px -680px; } -.emoji-paperclip { background-position: -360px -680px; } -.emoji-paperclips { background-position: -380px -680px; } -.emoji-park { background-position: -400px -680px; } -.emoji-parking { background-position: -420px -680px; } -.emoji-part_alternation_mark { background-position: -440px -680px; } -.emoji-partly_sunny { background-position: -460px -680px; } -.emoji-passport_control { background-position: -480px -680px; } -.emoji-pause_button { background-position: -500px -680px; } -.emoji-peace { background-position: -520px -680px; } -.emoji-peach { background-position: -540px -680px; } -.emoji-peanuts { background-position: -560px -680px; } -.emoji-pear { background-position: -580px -680px; } -.emoji-pen_ballpoint { background-position: -600px -680px; } -.emoji-pen_fountain { background-position: -620px -680px; } -.emoji-pencil { background-position: -640px -680px; } -.emoji-pencil2 { background-position: -660px -680px; } -.emoji-penguin { background-position: -680px -680px; } -.emoji-pensive { background-position: -700px 0; } -.emoji-performing_arts { background-position: -700px -20px; } -.emoji-persevere { background-position: -700px -40px; } -.emoji-person_frowning { background-position: -700px -60px; } -.emoji-person_frowning_tone1 { background-position: -700px -80px; } -.emoji-person_frowning_tone2 { background-position: -700px -100px; } -.emoji-person_frowning_tone3 { background-position: -700px -120px; } -.emoji-person_frowning_tone4 { background-position: -700px -140px; } -.emoji-person_frowning_tone5 { background-position: -700px -160px; } -.emoji-person_with_blond_hair { background-position: -700px -180px; } -.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; } -.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; } -.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; } -.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; } -.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; } -.emoji-person_with_pouting_face { background-position: -700px -300px; } -.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; } -.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; } -.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; } -.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; } -.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; } -.emoji-pick { background-position: -700px -420px; } -.emoji-pig { background-position: -700px -440px; } -.emoji-pig2 { background-position: -700px -460px; } -.emoji-pig_nose { background-position: -700px -480px; } -.emoji-pill { background-position: -700px -500px; } -.emoji-pineapple { background-position: -700px -520px; } -.emoji-ping_pong { background-position: -700px -540px; } -.emoji-pisces { background-position: -700px -560px; } -.emoji-pizza { background-position: -700px -580px; } -.emoji-place_of_worship { background-position: -700px -600px; } -.emoji-play_pause { background-position: -700px -620px; } -.emoji-point_down { background-position: -700px -640px; } -.emoji-point_down_tone1 { background-position: -700px -660px; } -.emoji-point_down_tone2 { background-position: -700px -680px; } -.emoji-point_down_tone3 { background-position: 0 -700px; } -.emoji-point_down_tone4 { background-position: -20px -700px; } -.emoji-point_down_tone5 { background-position: -40px -700px; } -.emoji-point_left { background-position: -60px -700px; } -.emoji-point_left_tone1 { background-position: -80px -700px; } -.emoji-point_left_tone2 { background-position: -100px -700px; } -.emoji-point_left_tone3 { background-position: -120px -700px; } -.emoji-point_left_tone4 { background-position: -140px -700px; } -.emoji-point_left_tone5 { background-position: -160px -700px; } -.emoji-point_right { background-position: -180px -700px; } -.emoji-point_right_tone1 { background-position: -200px -700px; } -.emoji-point_right_tone2 { background-position: -220px -700px; } -.emoji-point_right_tone3 { background-position: -240px -700px; } -.emoji-point_right_tone4 { background-position: -260px -700px; } -.emoji-point_right_tone5 { background-position: -280px -700px; } -.emoji-point_up { background-position: -300px -700px; } -.emoji-point_up_2 { background-position: -320px -700px; } -.emoji-point_up_2_tone1 { background-position: -340px -700px; } -.emoji-point_up_2_tone2 { background-position: -360px -700px; } -.emoji-point_up_2_tone3 { background-position: -380px -700px; } -.emoji-point_up_2_tone4 { background-position: -400px -700px; } -.emoji-point_up_2_tone5 { background-position: -420px -700px; } -.emoji-point_up_tone1 { background-position: -440px -700px; } -.emoji-point_up_tone2 { background-position: -460px -700px; } -.emoji-point_up_tone3 { background-position: -480px -700px; } -.emoji-point_up_tone4 { background-position: -500px -700px; } -.emoji-point_up_tone5 { background-position: -520px -700px; } -.emoji-police_car { background-position: -540px -700px; } -.emoji-poodle { background-position: -560px -700px; } -.emoji-poop { background-position: -580px -700px; } -.emoji-popcorn { background-position: -600px -700px; } -.emoji-post_office { background-position: -620px -700px; } -.emoji-postal_horn { background-position: -640px -700px; } -.emoji-postbox { background-position: -660px -700px; } -.emoji-potable_water { background-position: -680px -700px; } -.emoji-potato { background-position: -700px -700px; } -.emoji-pouch { background-position: -720px 0; } -.emoji-poultry_leg { background-position: -720px -20px; } -.emoji-pound { background-position: -720px -40px; } -.emoji-pouting_cat { background-position: -720px -60px; } -.emoji-pray { background-position: -720px -80px; } -.emoji-pray_tone1 { background-position: -720px -100px; } -.emoji-pray_tone2 { background-position: -720px -120px; } -.emoji-pray_tone3 { background-position: -720px -140px; } -.emoji-pray_tone4 { background-position: -720px -160px; } -.emoji-pray_tone5 { background-position: -720px -180px; } -.emoji-prayer_beads { background-position: -720px -200px; } -.emoji-pregnant_woman { background-position: -720px -220px; } -.emoji-pregnant_woman_tone1 { background-position: -720px -240px; } -.emoji-pregnant_woman_tone2 { background-position: -720px -260px; } -.emoji-pregnant_woman_tone3 { background-position: -720px -280px; } -.emoji-pregnant_woman_tone4 { background-position: -720px -300px; } -.emoji-pregnant_woman_tone5 { background-position: -720px -320px; } -.emoji-prince { background-position: -720px -340px; } -.emoji-prince_tone1 { background-position: -720px -360px; } -.emoji-prince_tone2 { background-position: -720px -380px; } -.emoji-prince_tone3 { background-position: -720px -400px; } -.emoji-prince_tone4 { background-position: -720px -420px; } -.emoji-prince_tone5 { background-position: -720px -440px; } -.emoji-princess { background-position: -720px -460px; } -.emoji-princess_tone1 { background-position: -720px -480px; } -.emoji-princess_tone2 { background-position: -720px -500px; } -.emoji-princess_tone3 { background-position: -720px -520px; } -.emoji-princess_tone4 { background-position: -720px -540px; } -.emoji-princess_tone5 { background-position: -720px -560px; } -.emoji-printer { background-position: -720px -580px; } -.emoji-projector { background-position: -720px -600px; } -.emoji-punch { background-position: -720px -620px; } -.emoji-punch_tone1 { background-position: -720px -640px; } -.emoji-punch_tone2 { background-position: -720px -660px; } -.emoji-punch_tone3 { background-position: -720px -680px; } -.emoji-punch_tone4 { background-position: -720px -700px; } -.emoji-punch_tone5 { background-position: 0 -720px; } -.emoji-purple_heart { background-position: -20px -720px; } -.emoji-purse { background-position: -40px -720px; } -.emoji-pushpin { background-position: -60px -720px; } -.emoji-put_litter_in_its_place { background-position: -80px -720px; } -.emoji-question { background-position: -100px -720px; } -.emoji-rabbit { background-position: -120px -720px; } -.emoji-rabbit2 { background-position: -140px -720px; } -.emoji-race_car { background-position: -160px -720px; } -.emoji-racehorse { background-position: -180px -720px; } -.emoji-radio { background-position: -200px -720px; } -.emoji-radio_button { background-position: -220px -720px; } -.emoji-radioactive { background-position: -240px -720px; } -.emoji-rage { background-position: -260px -720px; } -.emoji-railway_car { background-position: -280px -720px; } -.emoji-railway_track { background-position: -300px -720px; } -.emoji-rainbow { background-position: -320px -720px; } -.emoji-raised_back_of_hand { background-position: -340px -720px; } -.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; } -.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; } -.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; } -.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; } -.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; } -.emoji-raised_hand { background-position: -460px -720px; } -.emoji-raised_hand_tone1 { background-position: -480px -720px; } -.emoji-raised_hand_tone2 { background-position: -500px -720px; } -.emoji-raised_hand_tone3 { background-position: -520px -720px; } -.emoji-raised_hand_tone4 { background-position: -540px -720px; } -.emoji-raised_hand_tone5 { background-position: -560px -720px; } -.emoji-raised_hands { background-position: -580px -720px; } -.emoji-raised_hands_tone1 { background-position: -600px -720px; } -.emoji-raised_hands_tone2 { background-position: -620px -720px; } -.emoji-raised_hands_tone3 { background-position: -640px -720px; } -.emoji-raised_hands_tone4 { background-position: -660px -720px; } -.emoji-raised_hands_tone5 { background-position: -680px -720px; } -.emoji-raising_hand { background-position: -700px -720px; } -.emoji-raising_hand_tone1 { background-position: -720px -720px; } -.emoji-raising_hand_tone2 { background-position: -740px 0; } -.emoji-raising_hand_tone3 { background-position: -740px -20px; } -.emoji-raising_hand_tone4 { background-position: -740px -40px; } -.emoji-raising_hand_tone5 { background-position: -740px -60px; } -.emoji-ram { background-position: -740px -80px; } -.emoji-ramen { background-position: -740px -100px; } -.emoji-rat { background-position: -740px -120px; } -.emoji-record_button { background-position: -740px -140px; } -.emoji-recycle { background-position: -740px -160px; } -.emoji-red_car { background-position: -740px -180px; } -.emoji-red_circle { background-position: -740px -200px; } -.emoji-registered { background-position: -740px -220px; } -.emoji-relaxed { background-position: -740px -240px; } -.emoji-relieved { background-position: -740px -260px; } -.emoji-reminder_ribbon { background-position: -740px -280px; } -.emoji-repeat { background-position: -740px -300px; } -.emoji-repeat_one { background-position: -740px -320px; } -.emoji-restroom { background-position: -740px -340px; } -.emoji-revolving_hearts { background-position: -740px -360px; } -.emoji-rewind { background-position: -740px -380px; } -.emoji-rhino { background-position: -740px -400px; } -.emoji-ribbon { background-position: -740px -420px; } -.emoji-rice { background-position: -740px -440px; } -.emoji-rice_ball { background-position: -740px -460px; } -.emoji-rice_cracker { background-position: -740px -480px; } -.emoji-rice_scene { background-position: -740px -500px; } -.emoji-right_facing_fist { background-position: -740px -520px; } -.emoji-right_facing_fist_tone1 { background-position: -740px -540px; } -.emoji-right_facing_fist_tone2 { background-position: -740px -560px; } -.emoji-right_facing_fist_tone3 { background-position: -740px -580px; } -.emoji-right_facing_fist_tone4 { background-position: -740px -600px; } -.emoji-right_facing_fist_tone5 { background-position: -740px -620px; } -.emoji-ring { background-position: -740px -640px; } -.emoji-robot { background-position: -740px -660px; } -.emoji-rocket { background-position: -740px -680px; } -.emoji-rofl { background-position: -740px -700px; } -.emoji-roller_coaster { background-position: -740px -720px; } -.emoji-rolling_eyes { background-position: 0 -740px; } -.emoji-rooster { background-position: -20px -740px; } -.emoji-rose { background-position: -40px -740px; } -.emoji-rosette { background-position: -60px -740px; } -.emoji-rotating_light { background-position: -80px -740px; } -.emoji-round_pushpin { background-position: -100px -740px; } -.emoji-rowboat { background-position: -120px -740px; } -.emoji-rowboat_tone1 { background-position: -140px -740px; } -.emoji-rowboat_tone2 { background-position: -160px -740px; } -.emoji-rowboat_tone3 { background-position: -180px -740px; } -.emoji-rowboat_tone4 { background-position: -200px -740px; } -.emoji-rowboat_tone5 { background-position: -220px -740px; } -.emoji-rugby_football { background-position: -240px -740px; } -.emoji-runner { background-position: -260px -740px; } -.emoji-runner_tone1 { background-position: -280px -740px; } -.emoji-runner_tone2 { background-position: -300px -740px; } -.emoji-runner_tone3 { background-position: -320px -740px; } -.emoji-runner_tone4 { background-position: -340px -740px; } -.emoji-runner_tone5 { background-position: -360px -740px; } -.emoji-running_shirt_with_sash { background-position: -380px -740px; } -.emoji-sa { background-position: -400px -740px; } -.emoji-sagittarius { background-position: -420px -740px; } -.emoji-sailboat { background-position: -440px -740px; } -.emoji-sake { background-position: -460px -740px; } -.emoji-salad { background-position: -480px -740px; } -.emoji-sandal { background-position: -500px -740px; } -.emoji-santa { background-position: -520px -740px; } -.emoji-santa_tone1 { background-position: -540px -740px; } -.emoji-santa_tone2 { background-position: -560px -740px; } -.emoji-santa_tone3 { background-position: -580px -740px; } -.emoji-santa_tone4 { background-position: -600px -740px; } -.emoji-santa_tone5 { background-position: -620px -740px; } -.emoji-satellite { background-position: -640px -740px; } -.emoji-satellite_orbital { background-position: -660px -740px; } -.emoji-saxophone { background-position: -680px -740px; } -.emoji-scales { background-position: -700px -740px; } -.emoji-school { background-position: -720px -740px; } -.emoji-school_satchel { background-position: -740px -740px; } -.emoji-scissors { background-position: -760px 0; } -.emoji-scooter { background-position: -760px -20px; } -.emoji-scorpion { background-position: -760px -40px; } -.emoji-scorpius { background-position: -760px -60px; } -.emoji-scream { background-position: -760px -80px; } -.emoji-scream_cat { background-position: -760px -100px; } -.emoji-scroll { background-position: -760px -120px; } -.emoji-seat { background-position: -760px -140px; } -.emoji-second_place { background-position: -760px -160px; } -.emoji-secret { background-position: -760px -180px; } -.emoji-see_no_evil { background-position: -760px -200px; } -.emoji-seedling { background-position: -760px -220px; } -.emoji-selfie { background-position: -760px -240px; } -.emoji-selfie_tone1 { background-position: -760px -260px; } -.emoji-selfie_tone2 { background-position: -760px -280px; } -.emoji-selfie_tone3 { background-position: -760px -300px; } -.emoji-selfie_tone4 { background-position: -760px -320px; } -.emoji-selfie_tone5 { background-position: -760px -340px; } -.emoji-seven { background-position: -760px -360px; } -.emoji-shallow_pan_of_food { background-position: -760px -380px; } -.emoji-shamrock { background-position: -760px -400px; } -.emoji-shark { background-position: -760px -420px; } -.emoji-shaved_ice { background-position: -760px -440px; } -.emoji-sheep { background-position: -760px -460px; } -.emoji-shell { background-position: -760px -480px; } -.emoji-shield { background-position: -760px -500px; } -.emoji-shinto_shrine { background-position: -760px -520px; } -.emoji-ship { background-position: -760px -540px; } -.emoji-shirt { background-position: -760px -560px; } -.emoji-shopping_bags { background-position: -760px -580px; } -.emoji-shopping_cart { background-position: -760px -600px; } -.emoji-shower { background-position: -760px -620px; } -.emoji-shrimp { background-position: -760px -640px; } -.emoji-shrug { background-position: -760px -660px; } -.emoji-shrug_tone1 { background-position: -760px -680px; } -.emoji-shrug_tone2 { background-position: -760px -700px; } -.emoji-shrug_tone3 { background-position: -760px -720px; } -.emoji-shrug_tone4 { background-position: -760px -740px; } -.emoji-shrug_tone5 { background-position: 0 -760px; } -.emoji-signal_strength { background-position: -20px -760px; } -.emoji-six { background-position: -40px -760px; } -.emoji-six_pointed_star { background-position: -60px -760px; } -.emoji-ski { background-position: -80px -760px; } -.emoji-skier { background-position: -100px -760px; } -.emoji-skull { background-position: -120px -760px; } -.emoji-skull_crossbones { background-position: -140px -760px; } -.emoji-sleeping { background-position: -160px -760px; } -.emoji-sleeping_accommodation { background-position: -180px -760px; } -.emoji-sleepy { background-position: -200px -760px; } -.emoji-slight_frown { background-position: -220px -760px; } -.emoji-slight_smile { background-position: -240px -760px; } -.emoji-slot_machine { background-position: -260px -760px; } -.emoji-small_blue_diamond { background-position: -280px -760px; } -.emoji-small_orange_diamond { background-position: -300px -760px; } -.emoji-small_red_triangle { background-position: -320px -760px; } -.emoji-small_red_triangle_down { background-position: -340px -760px; } -.emoji-smile { background-position: -360px -760px; } -.emoji-smile_cat { background-position: -380px -760px; } -.emoji-smiley { background-position: -400px -760px; } -.emoji-smiley_cat { background-position: -420px -760px; } -.emoji-smiling_imp { background-position: -440px -760px; } -.emoji-smirk { background-position: -460px -760px; } -.emoji-smirk_cat { background-position: -480px -760px; } -.emoji-smoking { background-position: -500px -760px; } -.emoji-snail { background-position: -520px -760px; } -.emoji-snake { background-position: -540px -760px; } -.emoji-sneezing_face { background-position: -560px -760px; } -.emoji-snowboarder { background-position: -580px -760px; } -.emoji-snowflake { background-position: -600px -760px; } -.emoji-snowman { background-position: -620px -760px; } -.emoji-snowman2 { background-position: -640px -760px; } -.emoji-sob { background-position: -660px -760px; } -.emoji-soccer { background-position: -680px -760px; } -.emoji-soon { background-position: -700px -760px; } -.emoji-sos { background-position: -720px -760px; } -.emoji-sound { background-position: -740px -760px; } -.emoji-space_invader { background-position: -760px -760px; } -.emoji-spades { background-position: -780px 0; } -.emoji-spaghetti { background-position: -780px -20px; } -.emoji-sparkle { background-position: -780px -40px; } -.emoji-sparkler { background-position: -780px -60px; } -.emoji-sparkles { background-position: -780px -80px; } -.emoji-sparkling_heart { background-position: -780px -100px; } -.emoji-speak_no_evil { background-position: -780px -120px; } -.emoji-speaker { background-position: -780px -140px; } -.emoji-speaking_head { background-position: -780px -160px; } -.emoji-speech_balloon { background-position: -780px -180px; } -.emoji-speedboat { background-position: -780px -200px; } -.emoji-spider { background-position: -780px -220px; } -.emoji-spider_web { background-position: -780px -240px; } -.emoji-spoon { background-position: -780px -260px; } -.emoji-spy { background-position: -780px -280px; } -.emoji-spy_tone1 { background-position: -780px -300px; } -.emoji-spy_tone2 { background-position: -780px -320px; } -.emoji-spy_tone3 { background-position: -780px -340px; } -.emoji-spy_tone4 { background-position: -780px -360px; } -.emoji-spy_tone5 { background-position: -780px -380px; } -.emoji-squid { background-position: -780px -400px; } -.emoji-stadium { background-position: -780px -420px; } -.emoji-star { background-position: -780px -440px; } -.emoji-star2 { background-position: -780px -460px; } -.emoji-star_and_crescent { background-position: -780px -480px; } -.emoji-star_of_david { background-position: -780px -500px; } -.emoji-stars { background-position: -780px -520px; } -.emoji-station { background-position: -780px -540px; } -.emoji-statue_of_liberty { background-position: -780px -560px; } -.emoji-steam_locomotive { background-position: -780px -580px; } -.emoji-stew { background-position: -780px -600px; } -.emoji-stop_button { background-position: -780px -620px; } -.emoji-stopwatch { background-position: -780px -640px; } -.emoji-straight_ruler { background-position: -780px -660px; } -.emoji-strawberry { background-position: -780px -680px; } -.emoji-stuck_out_tongue { background-position: -780px -700px; } -.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; } -.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; } -.emoji-stuffed_flatbread { background-position: -780px -760px; } -.emoji-sun_with_face { background-position: 0 -780px; } -.emoji-sunflower { background-position: -20px -780px; } -.emoji-sunglasses { background-position: -40px -780px; } -.emoji-sunny { background-position: -60px -780px; } -.emoji-sunrise { background-position: -80px -780px; } -.emoji-sunrise_over_mountains { background-position: -100px -780px; } -.emoji-surfer { background-position: -120px -780px; } -.emoji-surfer_tone1 { background-position: -140px -780px; } -.emoji-surfer_tone2 { background-position: -160px -780px; } -.emoji-surfer_tone3 { background-position: -180px -780px; } -.emoji-surfer_tone4 { background-position: -200px -780px; } -.emoji-surfer_tone5 { background-position: -220px -780px; } -.emoji-sushi { background-position: -240px -780px; } -.emoji-suspension_railway { background-position: -260px -780px; } -.emoji-sweat { background-position: -280px -780px; } -.emoji-sweat_drops { background-position: -300px -780px; } -.emoji-sweat_smile { background-position: -320px -780px; } -.emoji-sweet_potato { background-position: -340px -780px; } -.emoji-swimmer { background-position: -360px -780px; } -.emoji-swimmer_tone1 { background-position: -380px -780px; } -.emoji-swimmer_tone2 { background-position: -400px -780px; } -.emoji-swimmer_tone3 { background-position: -420px -780px; } -.emoji-swimmer_tone4 { background-position: -440px -780px; } -.emoji-swimmer_tone5 { background-position: -460px -780px; } -.emoji-symbols { background-position: -480px -780px; } -.emoji-synagogue { background-position: -500px -780px; } -.emoji-syringe { background-position: -520px -780px; } -.emoji-taco { background-position: -540px -780px; } -.emoji-tada { background-position: -560px -780px; } -.emoji-tanabata_tree { background-position: -580px -780px; } -.emoji-tangerine { background-position: -600px -780px; } -.emoji-taurus { background-position: -620px -780px; } -.emoji-taxi { background-position: -640px -780px; } -.emoji-tea { background-position: -660px -780px; } -.emoji-telephone { background-position: -680px -780px; } -.emoji-telephone_receiver { background-position: -700px -780px; } -.emoji-telescope { background-position: -720px -780px; } -.emoji-ten { background-position: -740px -780px; } -.emoji-tennis { background-position: -760px -780px; } -.emoji-tent { background-position: -780px -780px; } -.emoji-thermometer { background-position: -800px 0; } -.emoji-thermometer_face { background-position: -800px -20px; } -.emoji-thinking { background-position: -800px -40px; } -.emoji-third_place { background-position: -800px -60px; } -.emoji-thought_balloon { background-position: -800px -80px; } -.emoji-three { background-position: -800px -100px; } -.emoji-thumbsdown { background-position: -800px -120px; } -.emoji-thumbsdown_tone1 { background-position: -800px -140px; } -.emoji-thumbsdown_tone2 { background-position: -800px -160px; } -.emoji-thumbsdown_tone3 { background-position: -800px -180px; } -.emoji-thumbsdown_tone4 { background-position: -800px -200px; } -.emoji-thumbsdown_tone5 { background-position: -800px -220px; } -.emoji-thumbsup { background-position: -800px -240px; } -.emoji-thumbsup_tone1 { background-position: -800px -260px; } -.emoji-thumbsup_tone2 { background-position: -800px -280px; } -.emoji-thumbsup_tone3 { background-position: -800px -300px; } -.emoji-thumbsup_tone4 { background-position: -800px -320px; } -.emoji-thumbsup_tone5 { background-position: -800px -340px; } -.emoji-thunder_cloud_rain { background-position: -800px -360px; } -.emoji-ticket { background-position: -800px -380px; } -.emoji-tickets { background-position: -800px -400px; } -.emoji-tiger { background-position: -800px -420px; } -.emoji-tiger2 { background-position: -800px -440px; } -.emoji-timer { background-position: -800px -460px; } -.emoji-tired_face { background-position: -800px -480px; } -.emoji-tm { background-position: -800px -500px; } -.emoji-toilet { background-position: -800px -520px; } -.emoji-tokyo_tower { background-position: -800px -540px; } -.emoji-tomato { background-position: -800px -560px; } -.emoji-tone1 { background-position: -800px -580px; } -.emoji-tone2 { background-position: -800px -600px; } -.emoji-tone3 { background-position: -800px -620px; } -.emoji-tone4 { background-position: -800px -640px; } -.emoji-tone5 { background-position: -800px -660px; } -.emoji-tongue { background-position: -800px -680px; } -.emoji-tools { background-position: -800px -700px; } -.emoji-top { background-position: -800px -720px; } -.emoji-tophat { background-position: -800px -740px; } -.emoji-track_next { background-position: -800px -760px; } -.emoji-track_previous { background-position: -800px -780px; } -.emoji-trackball { background-position: 0 -800px; } -.emoji-tractor { background-position: -20px -800px; } -.emoji-traffic_light { background-position: -40px -800px; } -.emoji-train { background-position: -60px -800px; } -.emoji-train2 { background-position: -80px -800px; } -.emoji-tram { background-position: -100px -800px; } -.emoji-triangular_flag_on_post { background-position: -120px -800px; } -.emoji-triangular_ruler { background-position: -140px -800px; } -.emoji-trident { background-position: -160px -800px; } -.emoji-triumph { background-position: -180px -800px; } -.emoji-trolleybus { background-position: -200px -800px; } -.emoji-trophy { background-position: -220px -800px; } -.emoji-tropical_drink { background-position: -240px -800px; } -.emoji-tropical_fish { background-position: -260px -800px; } -.emoji-truck { background-position: -280px -800px; } -.emoji-trumpet { background-position: -300px -800px; } -.emoji-tulip { background-position: -320px -800px; } -.emoji-tumbler_glass { background-position: -340px -800px; } -.emoji-turkey { background-position: -360px -800px; } -.emoji-turtle { background-position: -380px -800px; } -.emoji-tv { background-position: -400px -800px; } -.emoji-twisted_rightwards_arrows { background-position: -420px -800px; } -.emoji-two { background-position: -440px -800px; } -.emoji-two_hearts { background-position: -460px -800px; } -.emoji-two_men_holding_hands { background-position: -480px -800px; } -.emoji-two_women_holding_hands { background-position: -500px -800px; } -.emoji-u5272 { background-position: -520px -800px; } -.emoji-u5408 { background-position: -540px -800px; } -.emoji-u55b6 { background-position: -560px -800px; } -.emoji-u6307 { background-position: -580px -800px; } -.emoji-u6708 { background-position: -600px -800px; } -.emoji-u6709 { background-position: -620px -800px; } -.emoji-u6e80 { background-position: -640px -800px; } -.emoji-u7121 { background-position: -660px -800px; } -.emoji-u7533 { background-position: -680px -800px; } -.emoji-u7981 { background-position: -700px -800px; } -.emoji-u7a7a { background-position: -720px -800px; } -.emoji-umbrella { background-position: -740px -800px; } -.emoji-umbrella2 { background-position: -760px -800px; } -.emoji-unamused { background-position: -780px -800px; } -.emoji-underage { background-position: -800px -800px; } -.emoji-unicorn { background-position: -820px 0; } -.emoji-unlock { background-position: -820px -20px; } -.emoji-up { background-position: -820px -40px; } -.emoji-upside_down { background-position: -820px -60px; } -.emoji-urn { background-position: -820px -80px; } -.emoji-v { background-position: -820px -100px; } -.emoji-v_tone1 { background-position: -820px -120px; } -.emoji-v_tone2 { background-position: -820px -140px; } -.emoji-v_tone3 { background-position: -820px -160px; } -.emoji-v_tone4 { background-position: -820px -180px; } -.emoji-v_tone5 { background-position: -820px -200px; } -.emoji-vertical_traffic_light { background-position: -820px -220px; } -.emoji-vhs { background-position: -820px -240px; } -.emoji-vibration_mode { background-position: -820px -260px; } -.emoji-video_camera { background-position: -820px -280px; } -.emoji-video_game { background-position: -820px -300px; } -.emoji-violin { background-position: -820px -320px; } -.emoji-virgo { background-position: -820px -340px; } -.emoji-volcano { background-position: -820px -360px; } -.emoji-volleyball { background-position: -820px -380px; } -.emoji-vs { background-position: -820px -400px; } -.emoji-vulcan { background-position: -820px -420px; } -.emoji-vulcan_tone1 { background-position: -820px -440px; } -.emoji-vulcan_tone2 { background-position: -820px -460px; } -.emoji-vulcan_tone3 { background-position: -820px -480px; } -.emoji-vulcan_tone4 { background-position: -820px -500px; } -.emoji-vulcan_tone5 { background-position: -820px -520px; } -.emoji-walking { background-position: -820px -540px; } -.emoji-walking_tone1 { background-position: -820px -560px; } -.emoji-walking_tone2 { background-position: -820px -580px; } -.emoji-walking_tone3 { background-position: -820px -600px; } -.emoji-walking_tone4 { background-position: -820px -620px; } -.emoji-walking_tone5 { background-position: -820px -640px; } -.emoji-waning_crescent_moon { background-position: -820px -660px; } -.emoji-waning_gibbous_moon { background-position: -820px -680px; } -.emoji-warning { background-position: -820px -700px; } -.emoji-wastebasket { background-position: -820px -720px; } -.emoji-watch { background-position: -820px -740px; } -.emoji-water_buffalo { background-position: -820px -760px; } -.emoji-water_polo { background-position: -820px -780px; } -.emoji-water_polo_tone1 { background-position: -820px -800px; } -.emoji-water_polo_tone2 { background-position: 0 -820px; } -.emoji-water_polo_tone3 { background-position: -20px -820px; } -.emoji-water_polo_tone4 { background-position: -40px -820px; } -.emoji-water_polo_tone5 { background-position: -60px -820px; } -.emoji-watermelon { background-position: -80px -820px; } -.emoji-wave { background-position: -100px -820px; } -.emoji-wave_tone1 { background-position: -120px -820px; } -.emoji-wave_tone2 { background-position: -140px -820px; } -.emoji-wave_tone3 { background-position: -160px -820px; } -.emoji-wave_tone4 { background-position: -180px -820px; } -.emoji-wave_tone5 { background-position: -200px -820px; } -.emoji-wavy_dash { background-position: -220px -820px; } -.emoji-waxing_crescent_moon { background-position: -240px -820px; } -.emoji-waxing_gibbous_moon { background-position: -260px -820px; } -.emoji-wc { background-position: -280px -820px; } -.emoji-weary { background-position: -300px -820px; } -.emoji-wedding { background-position: -320px -820px; } -.emoji-whale { background-position: -340px -820px; } -.emoji-whale2 { background-position: -360px -820px; } -.emoji-wheel_of_dharma { background-position: -380px -820px; } -.emoji-wheelchair { background-position: -400px -820px; } -.emoji-white_check_mark { background-position: -420px -820px; } -.emoji-white_circle { background-position: -440px -820px; } -.emoji-white_flower { background-position: -460px -820px; } -.emoji-white_large_square { background-position: -480px -820px; } -.emoji-white_medium_small_square { background-position: -500px -820px; } -.emoji-white_medium_square { background-position: -520px -820px; } -.emoji-white_small_square { background-position: -540px -820px; } -.emoji-white_square_button { background-position: -560px -820px; } -.emoji-white_sun_cloud { background-position: -580px -820px; } -.emoji-white_sun_rain_cloud { background-position: -600px -820px; } -.emoji-white_sun_small_cloud { background-position: -620px -820px; } -.emoji-wilted_rose { background-position: -640px -820px; } -.emoji-wind_blowing_face { background-position: -660px -820px; } -.emoji-wind_chime { background-position: -680px -820px; } -.emoji-wine_glass { background-position: -700px -820px; } -.emoji-wink { background-position: -720px -820px; } -.emoji-wolf { background-position: -740px -820px; } -.emoji-woman { background-position: -760px -820px; } -.emoji-woman_tone1 { background-position: -780px -820px; } -.emoji-woman_tone2 { background-position: -800px -820px; } -.emoji-woman_tone3 { background-position: -820px -820px; } -.emoji-woman_tone4 { background-position: -840px 0; } -.emoji-woman_tone5 { background-position: -840px -20px; } -.emoji-womans_clothes { background-position: -840px -40px; } -.emoji-womans_hat { background-position: -840px -60px; } -.emoji-womens { background-position: -840px -80px; } -.emoji-worried { background-position: -840px -100px; } -.emoji-wrench { background-position: -840px -120px; } -.emoji-wrestlers { background-position: -840px -140px; } -.emoji-wrestlers_tone1 { background-position: -840px -160px; } -.emoji-wrestlers_tone2 { background-position: -840px -180px; } -.emoji-wrestlers_tone3 { background-position: -840px -200px; } -.emoji-wrestlers_tone4 { background-position: -840px -220px; } -.emoji-wrestlers_tone5 { background-position: -840px -240px; } -.emoji-writing_hand { background-position: -840px -260px; } -.emoji-writing_hand_tone1 { background-position: -840px -280px; } -.emoji-writing_hand_tone2 { background-position: -840px -300px; } -.emoji-writing_hand_tone3 { background-position: -840px -320px; } -.emoji-writing_hand_tone4 { background-position: -840px -340px; } -.emoji-writing_hand_tone5 { background-position: -840px -360px; } -.emoji-x { background-position: -840px -380px; } -.emoji-yellow_heart { background-position: -840px -400px; } -.emoji-yen { background-position: -840px -420px; } -.emoji-yin_yang { background-position: -840px -440px; } -.emoji-yum { background-position: -840px -460px; } -.emoji-zap { background-position: -840px -480px; } -.emoji-zero { background-position: -840px -500px; } -.emoji-zipper_mouth { background-position: -840px -520px; } -.emoji-100 { background-position: -840px -540px; } +.emoji-gay_pride_flag { background-position: -220px -540px; } +.emoji-gear { background-position: -240px -540px; } +.emoji-gem { background-position: -260px -540px; } +.emoji-gemini { background-position: -280px -540px; } +.emoji-ghost { background-position: -300px -540px; } +.emoji-gift { background-position: -320px -540px; } +.emoji-gift_heart { background-position: -340px -540px; } +.emoji-girl { background-position: -360px -540px; } +.emoji-girl_tone1 { background-position: -380px -540px; } +.emoji-girl_tone2 { background-position: -400px -540px; } +.emoji-girl_tone3 { background-position: -420px -540px; } +.emoji-girl_tone4 { background-position: -440px -540px; } +.emoji-girl_tone5 { background-position: -460px -540px; } +.emoji-globe_with_meridians { background-position: -480px -540px; } +.emoji-goal { background-position: -500px -540px; } +.emoji-goat { background-position: -520px -540px; } +.emoji-golf { background-position: -540px -540px; } +.emoji-golfer { background-position: -560px 0; } +.emoji-gorilla { background-position: -560px -20px; } +.emoji-grapes { background-position: -560px -40px; } +.emoji-green_apple { background-position: -560px -60px; } +.emoji-green_book { background-position: -560px -80px; } +.emoji-green_heart { background-position: -560px -100px; } +.emoji-grey_exclamation { background-position: -560px -120px; } +.emoji-grey_question { background-position: -560px -140px; } +.emoji-grimacing { background-position: -560px -160px; } +.emoji-grin { background-position: -560px -180px; } +.emoji-grinning { background-position: -560px -200px; } +.emoji-guardsman { background-position: -560px -220px; } +.emoji-guardsman_tone1 { background-position: -560px -240px; } +.emoji-guardsman_tone2 { background-position: -560px -260px; } +.emoji-guardsman_tone3 { background-position: -560px -280px; } +.emoji-guardsman_tone4 { background-position: -560px -300px; } +.emoji-guardsman_tone5 { background-position: -560px -320px; } +.emoji-guitar { background-position: -560px -340px; } +.emoji-gun { background-position: -560px -360px; } +.emoji-haircut { background-position: -560px -380px; } +.emoji-haircut_tone1 { background-position: -560px -400px; } +.emoji-haircut_tone2 { background-position: -560px -420px; } +.emoji-haircut_tone3 { background-position: -560px -440px; } +.emoji-haircut_tone4 { background-position: -560px -460px; } +.emoji-haircut_tone5 { background-position: -560px -480px; } +.emoji-hamburger { background-position: -560px -500px; } +.emoji-hammer { background-position: -560px -520px; } +.emoji-hammer_pick { background-position: -560px -540px; } +.emoji-hamster { background-position: 0 -560px; } +.emoji-hand_splayed { background-position: -20px -560px; } +.emoji-hand_splayed_tone1 { background-position: -40px -560px; } +.emoji-hand_splayed_tone2 { background-position: -60px -560px; } +.emoji-hand_splayed_tone3 { background-position: -80px -560px; } +.emoji-hand_splayed_tone4 { background-position: -100px -560px; } +.emoji-hand_splayed_tone5 { background-position: -120px -560px; } +.emoji-handbag { background-position: -140px -560px; } +.emoji-handball { background-position: -160px -560px; } +.emoji-handball_tone1 { background-position: -180px -560px; } +.emoji-handball_tone2 { background-position: -200px -560px; } +.emoji-handball_tone3 { background-position: -220px -560px; } +.emoji-handball_tone4 { background-position: -240px -560px; } +.emoji-handball_tone5 { background-position: -260px -560px; } +.emoji-handshake { background-position: -280px -560px; } +.emoji-handshake_tone1 { background-position: -300px -560px; } +.emoji-handshake_tone2 { background-position: -320px -560px; } +.emoji-handshake_tone3 { background-position: -340px -560px; } +.emoji-handshake_tone4 { background-position: -360px -560px; } +.emoji-handshake_tone5 { background-position: -380px -560px; } +.emoji-hash { background-position: -400px -560px; } +.emoji-hatched_chick { background-position: -420px -560px; } +.emoji-hatching_chick { background-position: -440px -560px; } +.emoji-head_bandage { background-position: -460px -560px; } +.emoji-headphones { background-position: -480px -560px; } +.emoji-hear_no_evil { background-position: -500px -560px; } +.emoji-heart { background-position: -520px -560px; } +.emoji-heart_decoration { background-position: -540px -560px; } +.emoji-heart_exclamation { background-position: -560px -560px; } +.emoji-heart_eyes { background-position: -580px 0; } +.emoji-heart_eyes_cat { background-position: -580px -20px; } +.emoji-heartbeat { background-position: -580px -40px; } +.emoji-heartpulse { background-position: -580px -60px; } +.emoji-hearts { background-position: -580px -80px; } +.emoji-heavy_check_mark { background-position: -580px -100px; } +.emoji-heavy_division_sign { background-position: -580px -120px; } +.emoji-heavy_dollar_sign { background-position: -580px -140px; } +.emoji-heavy_minus_sign { background-position: -580px -160px; } +.emoji-heavy_multiplication_x { background-position: -580px -180px; } +.emoji-heavy_plus_sign { background-position: -580px -200px; } +.emoji-helicopter { background-position: -580px -220px; } +.emoji-helmet_with_cross { background-position: -580px -240px; } +.emoji-herb { background-position: -580px -260px; } +.emoji-hibiscus { background-position: -580px -280px; } +.emoji-high_brightness { background-position: -580px -300px; } +.emoji-high_heel { background-position: -580px -320px; } +.emoji-hockey { background-position: -580px -340px; } +.emoji-hole { background-position: -580px -360px; } +.emoji-homes { background-position: -580px -380px; } +.emoji-honey_pot { background-position: -580px -400px; } +.emoji-horse { background-position: -580px -420px; } +.emoji-horse_racing { background-position: -580px -440px; } +.emoji-horse_racing_tone1 { background-position: -580px -460px; } +.emoji-horse_racing_tone2 { background-position: -580px -480px; } +.emoji-horse_racing_tone3 { background-position: -580px -500px; } +.emoji-horse_racing_tone4 { background-position: -580px -520px; } +.emoji-horse_racing_tone5 { background-position: -580px -540px; } +.emoji-hospital { background-position: -580px -560px; } +.emoji-hot_pepper { background-position: 0 -580px; } +.emoji-hotdog { background-position: -20px -580px; } +.emoji-hotel { background-position: -40px -580px; } +.emoji-hotsprings { background-position: -60px -580px; } +.emoji-hourglass { background-position: -80px -580px; } +.emoji-hourglass_flowing_sand { background-position: -100px -580px; } +.emoji-house { background-position: -120px -580px; } +.emoji-house_abandoned { background-position: -140px -580px; } +.emoji-house_with_garden { background-position: -160px -580px; } +.emoji-hugging { background-position: -180px -580px; } +.emoji-hushed { background-position: -200px -580px; } +.emoji-ice_cream { background-position: -220px -580px; } +.emoji-ice_skate { background-position: -240px -580px; } +.emoji-icecream { background-position: -260px -580px; } +.emoji-id { background-position: -280px -580px; } +.emoji-ideograph_advantage { background-position: -300px -580px; } +.emoji-imp { background-position: -320px -580px; } +.emoji-inbox_tray { background-position: -340px -580px; } +.emoji-incoming_envelope { background-position: -360px -580px; } +.emoji-information_desk_person { background-position: -380px -580px; } +.emoji-information_desk_person_tone1 { background-position: -400px -580px; } +.emoji-information_desk_person_tone2 { background-position: -420px -580px; } +.emoji-information_desk_person_tone3 { background-position: -440px -580px; } +.emoji-information_desk_person_tone4 { background-position: -460px -580px; } +.emoji-information_desk_person_tone5 { background-position: -480px -580px; } +.emoji-information_source { background-position: -500px -580px; } +.emoji-innocent { background-position: -520px -580px; } +.emoji-interrobang { background-position: -540px -580px; } +.emoji-iphone { background-position: -560px -580px; } +.emoji-island { background-position: -580px -580px; } +.emoji-izakaya_lantern { background-position: -600px 0; } +.emoji-jack_o_lantern { background-position: -600px -20px; } +.emoji-japan { background-position: -600px -40px; } +.emoji-japanese_castle { background-position: -600px -60px; } +.emoji-japanese_goblin { background-position: -600px -80px; } +.emoji-japanese_ogre { background-position: -600px -100px; } +.emoji-jeans { background-position: -600px -120px; } +.emoji-joy { background-position: -600px -140px; } +.emoji-joy_cat { background-position: -600px -160px; } +.emoji-joystick { background-position: -600px -180px; } +.emoji-juggling { background-position: -600px -200px; } +.emoji-juggling_tone1 { background-position: -600px -220px; } +.emoji-juggling_tone2 { background-position: -600px -240px; } +.emoji-juggling_tone3 { background-position: -600px -260px; } +.emoji-juggling_tone4 { background-position: -600px -280px; } +.emoji-juggling_tone5 { background-position: -600px -300px; } +.emoji-kaaba { background-position: -600px -320px; } +.emoji-key { background-position: -600px -340px; } +.emoji-key2 { background-position: -600px -360px; } +.emoji-keyboard { background-position: -600px -380px; } +.emoji-kimono { background-position: -600px -400px; } +.emoji-kiss { background-position: -600px -420px; } +.emoji-kiss_mm { background-position: -600px -440px; } +.emoji-kiss_ww { background-position: -600px -460px; } +.emoji-kissing { background-position: -600px -480px; } +.emoji-kissing_cat { background-position: -600px -500px; } +.emoji-kissing_closed_eyes { background-position: -600px -520px; } +.emoji-kissing_heart { background-position: -600px -540px; } +.emoji-kissing_smiling_eyes { background-position: -600px -560px; } +.emoji-kiwi { background-position: -600px -580px; } +.emoji-knife { background-position: 0 -600px; } +.emoji-koala { background-position: -20px -600px; } +.emoji-koko { background-position: -40px -600px; } +.emoji-label { background-position: -60px -600px; } +.emoji-large_blue_circle { background-position: -80px -600px; } +.emoji-large_blue_diamond { background-position: -100px -600px; } +.emoji-large_orange_diamond { background-position: -120px -600px; } +.emoji-last_quarter_moon { background-position: -140px -600px; } +.emoji-last_quarter_moon_with_face { background-position: -160px -600px; } +.emoji-laughing { background-position: -180px -600px; } +.emoji-leaves { background-position: -200px -600px; } +.emoji-ledger { background-position: -220px -600px; } +.emoji-left_facing_fist { background-position: -240px -600px; } +.emoji-left_facing_fist_tone1 { background-position: -260px -600px; } +.emoji-left_facing_fist_tone2 { background-position: -280px -600px; } +.emoji-left_facing_fist_tone3 { background-position: -300px -600px; } +.emoji-left_facing_fist_tone4 { background-position: -320px -600px; } +.emoji-left_facing_fist_tone5 { background-position: -340px -600px; } +.emoji-left_luggage { background-position: -360px -600px; } +.emoji-left_right_arrow { background-position: -380px -600px; } +.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; } +.emoji-lemon { background-position: -420px -600px; } +.emoji-leo { background-position: -440px -600px; } +.emoji-leopard { background-position: -460px -600px; } +.emoji-level_slider { background-position: -480px -600px; } +.emoji-levitate { background-position: -500px -600px; } +.emoji-libra { background-position: -520px -600px; } +.emoji-lifter { background-position: -540px -600px; } +.emoji-lifter_tone1 { background-position: -560px -600px; } +.emoji-lifter_tone2 { background-position: -580px -600px; } +.emoji-lifter_tone3 { background-position: -600px -600px; } +.emoji-lifter_tone4 { background-position: -620px 0; } +.emoji-lifter_tone5 { background-position: -620px -20px; } +.emoji-light_rail { background-position: -620px -40px; } +.emoji-link { background-position: -620px -60px; } +.emoji-lion_face { background-position: -620px -80px; } +.emoji-lips { background-position: -620px -100px; } +.emoji-lipstick { background-position: -620px -120px; } +.emoji-lizard { background-position: -620px -140px; } +.emoji-lock { background-position: -620px -160px; } +.emoji-lock_with_ink_pen { background-position: -620px -180px; } +.emoji-lollipop { background-position: -620px -200px; } +.emoji-loop { background-position: -620px -220px; } +.emoji-loud_sound { background-position: -620px -240px; } +.emoji-loudspeaker { background-position: -620px -260px; } +.emoji-love_hotel { background-position: -620px -280px; } +.emoji-love_letter { background-position: -620px -300px; } +.emoji-low_brightness { background-position: -620px -320px; } +.emoji-lying_face { background-position: -620px -340px; } +.emoji-m { background-position: -620px -360px; } +.emoji-mag { background-position: -620px -380px; } +.emoji-mag_right { background-position: -620px -400px; } +.emoji-mahjong { background-position: -620px -420px; } +.emoji-mailbox { background-position: -620px -440px; } +.emoji-mailbox_closed { background-position: -620px -460px; } +.emoji-mailbox_with_mail { background-position: -620px -480px; } +.emoji-mailbox_with_no_mail { background-position: -620px -500px; } +.emoji-man { background-position: -620px -520px; } +.emoji-man_dancing { background-position: -620px -540px; } +.emoji-man_dancing_tone1 { background-position: -620px -560px; } +.emoji-man_dancing_tone2 { background-position: -620px -580px; } +.emoji-man_dancing_tone3 { background-position: -620px -600px; } +.emoji-man_dancing_tone4 { background-position: 0 -620px; } +.emoji-man_dancing_tone5 { background-position: -20px -620px; } +.emoji-man_in_tuxedo { background-position: -40px -620px; } +.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; } +.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; } +.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; } +.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; } +.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; } +.emoji-man_tone1 { background-position: -160px -620px; } +.emoji-man_tone2 { background-position: -180px -620px; } +.emoji-man_tone3 { background-position: -200px -620px; } +.emoji-man_tone4 { background-position: -220px -620px; } +.emoji-man_tone5 { background-position: -240px -620px; } +.emoji-man_with_gua_pi_mao { background-position: -260px -620px; } +.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; } +.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; } +.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; } +.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; } +.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; } +.emoji-man_with_turban { background-position: -380px -620px; } +.emoji-man_with_turban_tone1 { background-position: -400px -620px; } +.emoji-man_with_turban_tone2 { background-position: -420px -620px; } +.emoji-man_with_turban_tone3 { background-position: -440px -620px; } +.emoji-man_with_turban_tone4 { background-position: -460px -620px; } +.emoji-man_with_turban_tone5 { background-position: -480px -620px; } +.emoji-mans_shoe { background-position: -500px -620px; } +.emoji-map { background-position: -520px -620px; } +.emoji-maple_leaf { background-position: -540px -620px; } +.emoji-martial_arts_uniform { background-position: -560px -620px; } +.emoji-mask { background-position: -580px -620px; } +.emoji-massage { background-position: -600px -620px; } +.emoji-massage_tone1 { background-position: -620px -620px; } +.emoji-massage_tone2 { background-position: -640px 0; } +.emoji-massage_tone3 { background-position: -640px -20px; } +.emoji-massage_tone4 { background-position: -640px -40px; } +.emoji-massage_tone5 { background-position: -640px -60px; } +.emoji-meat_on_bone { background-position: -640px -80px; } +.emoji-medal { background-position: -640px -100px; } +.emoji-mega { background-position: -640px -120px; } +.emoji-melon { background-position: -640px -140px; } +.emoji-menorah { background-position: -640px -160px; } +.emoji-mens { background-position: -640px -180px; } +.emoji-metal { background-position: -640px -200px; } +.emoji-metal_tone1 { background-position: -640px -220px; } +.emoji-metal_tone2 { background-position: -640px -240px; } +.emoji-metal_tone3 { background-position: -640px -260px; } +.emoji-metal_tone4 { background-position: -640px -280px; } +.emoji-metal_tone5 { background-position: -640px -300px; } +.emoji-metro { background-position: -640px -320px; } +.emoji-microphone { background-position: -640px -340px; } +.emoji-microphone2 { background-position: -640px -360px; } +.emoji-microscope { background-position: -640px -380px; } +.emoji-middle_finger { background-position: -640px -400px; } +.emoji-middle_finger_tone1 { background-position: -640px -420px; } +.emoji-middle_finger_tone2 { background-position: -640px -440px; } +.emoji-middle_finger_tone3 { background-position: -640px -460px; } +.emoji-middle_finger_tone4 { background-position: -640px -480px; } +.emoji-middle_finger_tone5 { background-position: -640px -500px; } +.emoji-military_medal { background-position: -640px -520px; } +.emoji-milk { background-position: -640px -540px; } +.emoji-milky_way { background-position: -640px -560px; } +.emoji-minibus { background-position: -640px -580px; } +.emoji-minidisc { background-position: -640px -600px; } +.emoji-mobile_phone_off { background-position: -640px -620px; } +.emoji-money_mouth { background-position: 0 -640px; } +.emoji-money_with_wings { background-position: -20px -640px; } +.emoji-moneybag { background-position: -40px -640px; } +.emoji-monkey { background-position: -60px -640px; } +.emoji-monkey_face { background-position: -80px -640px; } +.emoji-monorail { background-position: -100px -640px; } +.emoji-mortar_board { background-position: -120px -640px; } +.emoji-mosque { background-position: -140px -640px; } +.emoji-motor_scooter { background-position: -160px -640px; } +.emoji-motorboat { background-position: -180px -640px; } +.emoji-motorcycle { background-position: -200px -640px; } +.emoji-motorway { background-position: -220px -640px; } +.emoji-mount_fuji { background-position: -240px -640px; } +.emoji-mountain { background-position: -260px -640px; } +.emoji-mountain_bicyclist { background-position: -280px -640px; } +.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; } +.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; } +.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; } +.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; } +.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; } +.emoji-mountain_cableway { background-position: -400px -640px; } +.emoji-mountain_railway { background-position: -420px -640px; } +.emoji-mountain_snow { background-position: -440px -640px; } +.emoji-mouse { background-position: -460px -640px; } +.emoji-mouse2 { background-position: -480px -640px; } +.emoji-mouse_three_button { background-position: -500px -640px; } +.emoji-movie_camera { background-position: -520px -640px; } +.emoji-moyai { background-position: -540px -640px; } +.emoji-mrs_claus { background-position: -560px -640px; } +.emoji-mrs_claus_tone1 { background-position: -580px -640px; } +.emoji-mrs_claus_tone2 { background-position: -600px -640px; } +.emoji-mrs_claus_tone3 { background-position: -620px -640px; } +.emoji-mrs_claus_tone4 { background-position: -640px -640px; } +.emoji-mrs_claus_tone5 { background-position: -660px 0; } +.emoji-muscle { background-position: -660px -20px; } +.emoji-muscle_tone1 { background-position: -660px -40px; } +.emoji-muscle_tone2 { background-position: -660px -60px; } +.emoji-muscle_tone3 { background-position: -660px -80px; } +.emoji-muscle_tone4 { background-position: -660px -100px; } +.emoji-muscle_tone5 { background-position: -660px -120px; } +.emoji-mushroom { background-position: -660px -140px; } +.emoji-musical_keyboard { background-position: -660px -160px; } +.emoji-musical_note { background-position: -660px -180px; } +.emoji-musical_score { background-position: -660px -200px; } +.emoji-mute { background-position: -660px -220px; } +.emoji-nail_care { background-position: -660px -240px; } +.emoji-nail_care_tone1 { background-position: -660px -260px; } +.emoji-nail_care_tone2 { background-position: -660px -280px; } +.emoji-nail_care_tone3 { background-position: -660px -300px; } +.emoji-nail_care_tone4 { background-position: -660px -320px; } +.emoji-nail_care_tone5 { background-position: -660px -340px; } +.emoji-name_badge { background-position: -660px -360px; } +.emoji-nauseated_face { background-position: -660px -380px; } +.emoji-necktie { background-position: -660px -400px; } +.emoji-negative_squared_cross_mark { background-position: -660px -420px; } +.emoji-nerd { background-position: -660px -440px; } +.emoji-neutral_face { background-position: -660px -460px; } +.emoji-new { background-position: -660px -480px; } +.emoji-new_moon { background-position: -660px -500px; } +.emoji-new_moon_with_face { background-position: -660px -520px; } +.emoji-newspaper { background-position: -660px -540px; } +.emoji-newspaper2 { background-position: -660px -560px; } +.emoji-ng { background-position: -660px -580px; } +.emoji-night_with_stars { background-position: -660px -600px; } +.emoji-nine { background-position: -660px -620px; } +.emoji-no_bell { background-position: -660px -640px; } +.emoji-no_bicycles { background-position: 0 -660px; } +.emoji-no_entry { background-position: -20px -660px; } +.emoji-no_entry_sign { background-position: -40px -660px; } +.emoji-no_good { background-position: -60px -660px; } +.emoji-no_good_tone1 { background-position: -80px -660px; } +.emoji-no_good_tone2 { background-position: -100px -660px; } +.emoji-no_good_tone3 { background-position: -120px -660px; } +.emoji-no_good_tone4 { background-position: -140px -660px; } +.emoji-no_good_tone5 { background-position: -160px -660px; } +.emoji-no_mobile_phones { background-position: -180px -660px; } +.emoji-no_mouth { background-position: -200px -660px; } +.emoji-no_pedestrians { background-position: -220px -660px; } +.emoji-no_smoking { background-position: -240px -660px; } +.emoji-non-potable_water { background-position: -260px -660px; } +.emoji-nose { background-position: -280px -660px; } +.emoji-nose_tone1 { background-position: -300px -660px; } +.emoji-nose_tone2 { background-position: -320px -660px; } +.emoji-nose_tone3 { background-position: -340px -660px; } +.emoji-nose_tone4 { background-position: -360px -660px; } +.emoji-nose_tone5 { background-position: -380px -660px; } +.emoji-notebook { background-position: -400px -660px; } +.emoji-notebook_with_decorative_cover { background-position: -420px -660px; } +.emoji-notepad_spiral { background-position: -440px -660px; } +.emoji-notes { background-position: -460px -660px; } +.emoji-nut_and_bolt { background-position: -480px -660px; } +.emoji-o { background-position: -500px -660px; } +.emoji-o2 { background-position: -520px -660px; } +.emoji-ocean { background-position: -540px -660px; } +.emoji-octagonal_sign { background-position: -560px -660px; } +.emoji-octopus { background-position: -580px -660px; } +.emoji-oden { background-position: -600px -660px; } +.emoji-office { background-position: -620px -660px; } +.emoji-oil { background-position: -640px -660px; } +.emoji-ok { background-position: -660px -660px; } +.emoji-ok_hand { background-position: -680px 0; } +.emoji-ok_hand_tone1 { background-position: -680px -20px; } +.emoji-ok_hand_tone2 { background-position: -680px -40px; } +.emoji-ok_hand_tone3 { background-position: -680px -60px; } +.emoji-ok_hand_tone4 { background-position: -680px -80px; } +.emoji-ok_hand_tone5 { background-position: -680px -100px; } +.emoji-ok_woman { background-position: -680px -120px; } +.emoji-ok_woman_tone1 { background-position: -680px -140px; } +.emoji-ok_woman_tone2 { background-position: -680px -160px; } +.emoji-ok_woman_tone3 { background-position: -680px -180px; } +.emoji-ok_woman_tone4 { background-position: -680px -200px; } +.emoji-ok_woman_tone5 { background-position: -680px -220px; } +.emoji-older_man { background-position: -680px -240px; } +.emoji-older_man_tone1 { background-position: -680px -260px; } +.emoji-older_man_tone2 { background-position: -680px -280px; } +.emoji-older_man_tone3 { background-position: -680px -300px; } +.emoji-older_man_tone4 { background-position: -680px -320px; } +.emoji-older_man_tone5 { background-position: -680px -340px; } +.emoji-older_woman { background-position: -680px -360px; } +.emoji-older_woman_tone1 { background-position: -680px -380px; } +.emoji-older_woman_tone2 { background-position: -680px -400px; } +.emoji-older_woman_tone3 { background-position: -680px -420px; } +.emoji-older_woman_tone4 { background-position: -680px -440px; } +.emoji-older_woman_tone5 { background-position: -680px -460px; } +.emoji-om_symbol { background-position: -680px -480px; } +.emoji-on { background-position: -680px -500px; } +.emoji-oncoming_automobile { background-position: -680px -520px; } +.emoji-oncoming_bus { background-position: -680px -540px; } +.emoji-oncoming_police_car { background-position: -680px -560px; } +.emoji-oncoming_taxi { background-position: -680px -580px; } +.emoji-one { background-position: -680px -600px; } +.emoji-open_file_folder { background-position: -680px -620px; } +.emoji-open_hands { background-position: -680px -640px; } +.emoji-open_hands_tone1 { background-position: -680px -660px; } +.emoji-open_hands_tone2 { background-position: 0 -680px; } +.emoji-open_hands_tone3 { background-position: -20px -680px; } +.emoji-open_hands_tone4 { background-position: -40px -680px; } +.emoji-open_hands_tone5 { background-position: -60px -680px; } +.emoji-open_mouth { background-position: -80px -680px; } +.emoji-ophiuchus { background-position: -100px -680px; } +.emoji-orange_book { background-position: -120px -680px; } +.emoji-orthodox_cross { background-position: -140px -680px; } +.emoji-outbox_tray { background-position: -160px -680px; } +.emoji-owl { background-position: -180px -680px; } +.emoji-ox { background-position: -200px -680px; } +.emoji-package { background-position: -220px -680px; } +.emoji-page_facing_up { background-position: -240px -680px; } +.emoji-page_with_curl { background-position: -260px -680px; } +.emoji-pager { background-position: -280px -680px; } +.emoji-paintbrush { background-position: -300px -680px; } +.emoji-palm_tree { background-position: -320px -680px; } +.emoji-pancakes { background-position: -340px -680px; } +.emoji-panda_face { background-position: -360px -680px; } +.emoji-paperclip { background-position: -380px -680px; } +.emoji-paperclips { background-position: -400px -680px; } +.emoji-park { background-position: -420px -680px; } +.emoji-parking { background-position: -440px -680px; } +.emoji-part_alternation_mark { background-position: -460px -680px; } +.emoji-partly_sunny { background-position: -480px -680px; } +.emoji-passport_control { background-position: -500px -680px; } +.emoji-pause_button { background-position: -520px -680px; } +.emoji-peace { background-position: -540px -680px; } +.emoji-peach { background-position: -560px -680px; } +.emoji-peanuts { background-position: -580px -680px; } +.emoji-pear { background-position: -600px -680px; } +.emoji-pen_ballpoint { background-position: -620px -680px; } +.emoji-pen_fountain { background-position: -640px -680px; } +.emoji-pencil { background-position: -660px -680px; } +.emoji-pencil2 { background-position: -680px -680px; } +.emoji-penguin { background-position: -700px 0; } +.emoji-pensive { background-position: -700px -20px; } +.emoji-performing_arts { background-position: -700px -40px; } +.emoji-persevere { background-position: -700px -60px; } +.emoji-person_frowning { background-position: -700px -80px; } +.emoji-person_frowning_tone1 { background-position: -700px -100px; } +.emoji-person_frowning_tone2 { background-position: -700px -120px; } +.emoji-person_frowning_tone3 { background-position: -700px -140px; } +.emoji-person_frowning_tone4 { background-position: -700px -160px; } +.emoji-person_frowning_tone5 { background-position: -700px -180px; } +.emoji-person_with_blond_hair { background-position: -700px -200px; } +.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; } +.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; } +.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; } +.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; } +.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; } +.emoji-person_with_pouting_face { background-position: -700px -320px; } +.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; } +.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; } +.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; } +.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; } +.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; } +.emoji-pick { background-position: -700px -440px; } +.emoji-pig { background-position: -700px -460px; } +.emoji-pig2 { background-position: -700px -480px; } +.emoji-pig_nose { background-position: -700px -500px; } +.emoji-pill { background-position: -700px -520px; } +.emoji-pineapple { background-position: -700px -540px; } +.emoji-ping_pong { background-position: -700px -560px; } +.emoji-pisces { background-position: -700px -580px; } +.emoji-pizza { background-position: -700px -600px; } +.emoji-place_of_worship { background-position: -700px -620px; } +.emoji-play_pause { background-position: -700px -640px; } +.emoji-point_down { background-position: -700px -660px; } +.emoji-point_down_tone1 { background-position: -700px -680px; } +.emoji-point_down_tone2 { background-position: 0 -700px; } +.emoji-point_down_tone3 { background-position: -20px -700px; } +.emoji-point_down_tone4 { background-position: -40px -700px; } +.emoji-point_down_tone5 { background-position: -60px -700px; } +.emoji-point_left { background-position: -80px -700px; } +.emoji-point_left_tone1 { background-position: -100px -700px; } +.emoji-point_left_tone2 { background-position: -120px -700px; } +.emoji-point_left_tone3 { background-position: -140px -700px; } +.emoji-point_left_tone4 { background-position: -160px -700px; } +.emoji-point_left_tone5 { background-position: -180px -700px; } +.emoji-point_right { background-position: -200px -700px; } +.emoji-point_right_tone1 { background-position: -220px -700px; } +.emoji-point_right_tone2 { background-position: -240px -700px; } +.emoji-point_right_tone3 { background-position: -260px -700px; } +.emoji-point_right_tone4 { background-position: -280px -700px; } +.emoji-point_right_tone5 { background-position: -300px -700px; } +.emoji-point_up { background-position: -320px -700px; } +.emoji-point_up_2 { background-position: -340px -700px; } +.emoji-point_up_2_tone1 { background-position: -360px -700px; } +.emoji-point_up_2_tone2 { background-position: -380px -700px; } +.emoji-point_up_2_tone3 { background-position: -400px -700px; } +.emoji-point_up_2_tone4 { background-position: -420px -700px; } +.emoji-point_up_2_tone5 { background-position: -440px -700px; } +.emoji-point_up_tone1 { background-position: -460px -700px; } +.emoji-point_up_tone2 { background-position: -480px -700px; } +.emoji-point_up_tone3 { background-position: -500px -700px; } +.emoji-point_up_tone4 { background-position: -520px -700px; } +.emoji-point_up_tone5 { background-position: -540px -700px; } +.emoji-police_car { background-position: -560px -700px; } +.emoji-poodle { background-position: -580px -700px; } +.emoji-poop { background-position: -600px -700px; } +.emoji-popcorn { background-position: -620px -700px; } +.emoji-post_office { background-position: -640px -700px; } +.emoji-postal_horn { background-position: -660px -700px; } +.emoji-postbox { background-position: -680px -700px; } +.emoji-potable_water { background-position: -700px -700px; } +.emoji-potato { background-position: -720px 0; } +.emoji-pouch { background-position: -720px -20px; } +.emoji-poultry_leg { background-position: -720px -40px; } +.emoji-pound { background-position: -720px -60px; } +.emoji-pouting_cat { background-position: -720px -80px; } +.emoji-pray { background-position: -720px -100px; } +.emoji-pray_tone1 { background-position: -720px -120px; } +.emoji-pray_tone2 { background-position: -720px -140px; } +.emoji-pray_tone3 { background-position: -720px -160px; } +.emoji-pray_tone4 { background-position: -720px -180px; } +.emoji-pray_tone5 { background-position: -720px -200px; } +.emoji-prayer_beads { background-position: -720px -220px; } +.emoji-pregnant_woman { background-position: -720px -240px; } +.emoji-pregnant_woman_tone1 { background-position: -720px -260px; } +.emoji-pregnant_woman_tone2 { background-position: -720px -280px; } +.emoji-pregnant_woman_tone3 { background-position: -720px -300px; } +.emoji-pregnant_woman_tone4 { background-position: -720px -320px; } +.emoji-pregnant_woman_tone5 { background-position: -720px -340px; } +.emoji-prince { background-position: -720px -360px; } +.emoji-prince_tone1 { background-position: -720px -380px; } +.emoji-prince_tone2 { background-position: -720px -400px; } +.emoji-prince_tone3 { background-position: -720px -420px; } +.emoji-prince_tone4 { background-position: -720px -440px; } +.emoji-prince_tone5 { background-position: -720px -460px; } +.emoji-princess { background-position: -720px -480px; } +.emoji-princess_tone1 { background-position: -720px -500px; } +.emoji-princess_tone2 { background-position: -720px -520px; } +.emoji-princess_tone3 { background-position: -720px -540px; } +.emoji-princess_tone4 { background-position: -720px -560px; } +.emoji-princess_tone5 { background-position: -720px -580px; } +.emoji-printer { background-position: -720px -600px; } +.emoji-projector { background-position: -720px -620px; } +.emoji-punch { background-position: -720px -640px; } +.emoji-punch_tone1 { background-position: -720px -660px; } +.emoji-punch_tone2 { background-position: -720px -680px; } +.emoji-punch_tone3 { background-position: -720px -700px; } +.emoji-punch_tone4 { background-position: 0 -720px; } +.emoji-punch_tone5 { background-position: -20px -720px; } +.emoji-purple_heart { background-position: -40px -720px; } +.emoji-purse { background-position: -60px -720px; } +.emoji-pushpin { background-position: -80px -720px; } +.emoji-put_litter_in_its_place { background-position: -100px -720px; } +.emoji-question { background-position: -120px -720px; } +.emoji-rabbit { background-position: -140px -720px; } +.emoji-rabbit2 { background-position: -160px -720px; } +.emoji-race_car { background-position: -180px -720px; } +.emoji-racehorse { background-position: -200px -720px; } +.emoji-radio { background-position: -220px -720px; } +.emoji-radio_button { background-position: -240px -720px; } +.emoji-radioactive { background-position: -260px -720px; } +.emoji-rage { background-position: -280px -720px; } +.emoji-railway_car { background-position: -300px -720px; } +.emoji-railway_track { background-position: -320px -720px; } +.emoji-rainbow { background-position: -340px -720px; } +.emoji-raised_back_of_hand { background-position: -360px -720px; } +.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; } +.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; } +.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; } +.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; } +.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; } +.emoji-raised_hand { background-position: -480px -720px; } +.emoji-raised_hand_tone1 { background-position: -500px -720px; } +.emoji-raised_hand_tone2 { background-position: -520px -720px; } +.emoji-raised_hand_tone3 { background-position: -540px -720px; } +.emoji-raised_hand_tone4 { background-position: -560px -720px; } +.emoji-raised_hand_tone5 { background-position: -580px -720px; } +.emoji-raised_hands { background-position: -600px -720px; } +.emoji-raised_hands_tone1 { background-position: -620px -720px; } +.emoji-raised_hands_tone2 { background-position: -640px -720px; } +.emoji-raised_hands_tone3 { background-position: -660px -720px; } +.emoji-raised_hands_tone4 { background-position: -680px -720px; } +.emoji-raised_hands_tone5 { background-position: -700px -720px; } +.emoji-raising_hand { background-position: -720px -720px; } +.emoji-raising_hand_tone1 { background-position: -740px 0; } +.emoji-raising_hand_tone2 { background-position: -740px -20px; } +.emoji-raising_hand_tone3 { background-position: -740px -40px; } +.emoji-raising_hand_tone4 { background-position: -740px -60px; } +.emoji-raising_hand_tone5 { background-position: -740px -80px; } +.emoji-ram { background-position: -740px -100px; } +.emoji-ramen { background-position: -740px -120px; } +.emoji-rat { background-position: -740px -140px; } +.emoji-record_button { background-position: -740px -160px; } +.emoji-recycle { background-position: -740px -180px; } +.emoji-red_car { background-position: -740px -200px; } +.emoji-red_circle { background-position: -740px -220px; } +.emoji-registered { background-position: -740px -240px; } +.emoji-relaxed { background-position: -740px -260px; } +.emoji-relieved { background-position: -740px -280px; } +.emoji-reminder_ribbon { background-position: -740px -300px; } +.emoji-repeat { background-position: -740px -320px; } +.emoji-repeat_one { background-position: -740px -340px; } +.emoji-restroom { background-position: -740px -360px; } +.emoji-revolving_hearts { background-position: -740px -380px; } +.emoji-rewind { background-position: -740px -400px; } +.emoji-rhino { background-position: -740px -420px; } +.emoji-ribbon { background-position: -740px -440px; } +.emoji-rice { background-position: -740px -460px; } +.emoji-rice_ball { background-position: -740px -480px; } +.emoji-rice_cracker { background-position: -740px -500px; } +.emoji-rice_scene { background-position: -740px -520px; } +.emoji-right_facing_fist { background-position: -740px -540px; } +.emoji-right_facing_fist_tone1 { background-position: -740px -560px; } +.emoji-right_facing_fist_tone2 { background-position: -740px -580px; } +.emoji-right_facing_fist_tone3 { background-position: -740px -600px; } +.emoji-right_facing_fist_tone4 { background-position: -740px -620px; } +.emoji-right_facing_fist_tone5 { background-position: -740px -640px; } +.emoji-ring { background-position: -740px -660px; } +.emoji-robot { background-position: -740px -680px; } +.emoji-rocket { background-position: -740px -700px; } +.emoji-rofl { background-position: -740px -720px; } +.emoji-roller_coaster { background-position: 0 -740px; } +.emoji-rolling_eyes { background-position: -20px -740px; } +.emoji-rooster { background-position: -40px -740px; } +.emoji-rose { background-position: -60px -740px; } +.emoji-rosette { background-position: -80px -740px; } +.emoji-rotating_light { background-position: -100px -740px; } +.emoji-round_pushpin { background-position: -120px -740px; } +.emoji-rowboat { background-position: -140px -740px; } +.emoji-rowboat_tone1 { background-position: -160px -740px; } +.emoji-rowboat_tone2 { background-position: -180px -740px; } +.emoji-rowboat_tone3 { background-position: -200px -740px; } +.emoji-rowboat_tone4 { background-position: -220px -740px; } +.emoji-rowboat_tone5 { background-position: -240px -740px; } +.emoji-rugby_football { background-position: -260px -740px; } +.emoji-runner { background-position: -280px -740px; } +.emoji-runner_tone1 { background-position: -300px -740px; } +.emoji-runner_tone2 { background-position: -320px -740px; } +.emoji-runner_tone3 { background-position: -340px -740px; } +.emoji-runner_tone4 { background-position: -360px -740px; } +.emoji-runner_tone5 { background-position: -380px -740px; } +.emoji-running_shirt_with_sash { background-position: -400px -740px; } +.emoji-sa { background-position: -420px -740px; } +.emoji-sagittarius { background-position: -440px -740px; } +.emoji-sailboat { background-position: -460px -740px; } +.emoji-sake { background-position: -480px -740px; } +.emoji-salad { background-position: -500px -740px; } +.emoji-sandal { background-position: -520px -740px; } +.emoji-santa { background-position: -540px -740px; } +.emoji-santa_tone1 { background-position: -560px -740px; } +.emoji-santa_tone2 { background-position: -580px -740px; } +.emoji-santa_tone3 { background-position: -600px -740px; } +.emoji-santa_tone4 { background-position: -620px -740px; } +.emoji-santa_tone5 { background-position: -640px -740px; } +.emoji-satellite { background-position: -660px -740px; } +.emoji-satellite_orbital { background-position: -680px -740px; } +.emoji-saxophone { background-position: -700px -740px; } +.emoji-scales { background-position: -720px -740px; } +.emoji-school { background-position: -740px -740px; } +.emoji-school_satchel { background-position: -760px 0; } +.emoji-scissors { background-position: -760px -20px; } +.emoji-scooter { background-position: -760px -40px; } +.emoji-scorpion { background-position: -760px -60px; } +.emoji-scorpius { background-position: -760px -80px; } +.emoji-scream { background-position: -760px -100px; } +.emoji-scream_cat { background-position: -760px -120px; } +.emoji-scroll { background-position: -760px -140px; } +.emoji-seat { background-position: -760px -160px; } +.emoji-second_place { background-position: -760px -180px; } +.emoji-secret { background-position: -760px -200px; } +.emoji-see_no_evil { background-position: -760px -220px; } +.emoji-seedling { background-position: -760px -240px; } +.emoji-selfie { background-position: -760px -260px; } +.emoji-selfie_tone1 { background-position: -760px -280px; } +.emoji-selfie_tone2 { background-position: -760px -300px; } +.emoji-selfie_tone3 { background-position: -760px -320px; } +.emoji-selfie_tone4 { background-position: -760px -340px; } +.emoji-selfie_tone5 { background-position: -760px -360px; } +.emoji-seven { background-position: -760px -380px; } +.emoji-shallow_pan_of_food { background-position: -760px -400px; } +.emoji-shamrock { background-position: -760px -420px; } +.emoji-shark { background-position: -760px -440px; } +.emoji-shaved_ice { background-position: -760px -460px; } +.emoji-sheep { background-position: -760px -480px; } +.emoji-shell { background-position: -760px -500px; } +.emoji-shield { background-position: -760px -520px; } +.emoji-shinto_shrine { background-position: -760px -540px; } +.emoji-ship { background-position: -760px -560px; } +.emoji-shirt { background-position: -760px -580px; } +.emoji-shopping_bags { background-position: -760px -600px; } +.emoji-shopping_cart { background-position: -760px -620px; } +.emoji-shower { background-position: -760px -640px; } +.emoji-shrimp { background-position: -760px -660px; } +.emoji-shrug { background-position: -760px -680px; } +.emoji-shrug_tone1 { background-position: -760px -700px; } +.emoji-shrug_tone2 { background-position: -760px -720px; } +.emoji-shrug_tone3 { background-position: -760px -740px; } +.emoji-shrug_tone4 { background-position: 0 -760px; } +.emoji-shrug_tone5 { background-position: -20px -760px; } +.emoji-signal_strength { background-position: -40px -760px; } +.emoji-six { background-position: -60px -760px; } +.emoji-six_pointed_star { background-position: -80px -760px; } +.emoji-ski { background-position: -100px -760px; } +.emoji-skier { background-position: -120px -760px; } +.emoji-skull { background-position: -140px -760px; } +.emoji-skull_crossbones { background-position: -160px -760px; } +.emoji-sleeping { background-position: -180px -760px; } +.emoji-sleeping_accommodation { background-position: -200px -760px; } +.emoji-sleepy { background-position: -220px -760px; } +.emoji-slight_frown { background-position: -240px -760px; } +.emoji-slight_smile { background-position: -260px -760px; } +.emoji-slot_machine { background-position: -280px -760px; } +.emoji-small_blue_diamond { background-position: -300px -760px; } +.emoji-small_orange_diamond { background-position: -320px -760px; } +.emoji-small_red_triangle { background-position: -340px -760px; } +.emoji-small_red_triangle_down { background-position: -360px -760px; } +.emoji-smile { background-position: -380px -760px; } +.emoji-smile_cat { background-position: -400px -760px; } +.emoji-smiley { background-position: -420px -760px; } +.emoji-smiley_cat { background-position: -440px -760px; } +.emoji-smiling_imp { background-position: -460px -760px; } +.emoji-smirk { background-position: -480px -760px; } +.emoji-smirk_cat { background-position: -500px -760px; } +.emoji-smoking { background-position: -520px -760px; } +.emoji-snail { background-position: -540px -760px; } +.emoji-snake { background-position: -560px -760px; } +.emoji-sneezing_face { background-position: -580px -760px; } +.emoji-snowboarder { background-position: -600px -760px; } +.emoji-snowflake { background-position: -620px -760px; } +.emoji-snowman { background-position: -640px -760px; } +.emoji-snowman2 { background-position: -660px -760px; } +.emoji-sob { background-position: -680px -760px; } +.emoji-soccer { background-position: -700px -760px; } +.emoji-soon { background-position: -720px -760px; } +.emoji-sos { background-position: -740px -760px; } +.emoji-sound { background-position: -760px -760px; } +.emoji-space_invader { background-position: -780px 0; } +.emoji-spades { background-position: -780px -20px; } +.emoji-spaghetti { background-position: -780px -40px; } +.emoji-sparkle { background-position: -780px -60px; } +.emoji-sparkler { background-position: -780px -80px; } +.emoji-sparkles { background-position: -780px -100px; } +.emoji-sparkling_heart { background-position: -780px -120px; } +.emoji-speak_no_evil { background-position: -780px -140px; } +.emoji-speaker { background-position: -780px -160px; } +.emoji-speaking_head { background-position: -780px -180px; } +.emoji-speech_balloon { background-position: -780px -200px; } +.emoji-speech_left { background-position: -780px -220px; } +.emoji-speedboat { background-position: -780px -240px; } +.emoji-spider { background-position: -780px -260px; } +.emoji-spider_web { background-position: -780px -280px; } +.emoji-spoon { background-position: -780px -300px; } +.emoji-spy { background-position: -780px -320px; } +.emoji-spy_tone1 { background-position: -780px -340px; } +.emoji-spy_tone2 { background-position: -780px -360px; } +.emoji-spy_tone3 { background-position: -780px -380px; } +.emoji-spy_tone4 { background-position: -780px -400px; } +.emoji-spy_tone5 { background-position: -780px -420px; } +.emoji-squid { background-position: -780px -440px; } +.emoji-stadium { background-position: -780px -460px; } +.emoji-star { background-position: -780px -480px; } +.emoji-star2 { background-position: -780px -500px; } +.emoji-star_and_crescent { background-position: -780px -520px; } +.emoji-star_of_david { background-position: -780px -540px; } +.emoji-stars { background-position: -780px -560px; } +.emoji-station { background-position: -780px -580px; } +.emoji-statue_of_liberty { background-position: -780px -600px; } +.emoji-steam_locomotive { background-position: -780px -620px; } +.emoji-stew { background-position: -780px -640px; } +.emoji-stop_button { background-position: -780px -660px; } +.emoji-stopwatch { background-position: -780px -680px; } +.emoji-straight_ruler { background-position: -780px -700px; } +.emoji-strawberry { background-position: -780px -720px; } +.emoji-stuck_out_tongue { background-position: -780px -740px; } +.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; } +.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; } +.emoji-stuffed_flatbread { background-position: -20px -780px; } +.emoji-sun_with_face { background-position: -40px -780px; } +.emoji-sunflower { background-position: -60px -780px; } +.emoji-sunglasses { background-position: -80px -780px; } +.emoji-sunny { background-position: -100px -780px; } +.emoji-sunrise { background-position: -120px -780px; } +.emoji-sunrise_over_mountains { background-position: -140px -780px; } +.emoji-surfer { background-position: -160px -780px; } +.emoji-surfer_tone1 { background-position: -180px -780px; } +.emoji-surfer_tone2 { background-position: -200px -780px; } +.emoji-surfer_tone3 { background-position: -220px -780px; } +.emoji-surfer_tone4 { background-position: -240px -780px; } +.emoji-surfer_tone5 { background-position: -260px -780px; } +.emoji-sushi { background-position: -280px -780px; } +.emoji-suspension_railway { background-position: -300px -780px; } +.emoji-sweat { background-position: -320px -780px; } +.emoji-sweat_drops { background-position: -340px -780px; } +.emoji-sweat_smile { background-position: -360px -780px; } +.emoji-sweet_potato { background-position: -380px -780px; } +.emoji-swimmer { background-position: -400px -780px; } +.emoji-swimmer_tone1 { background-position: -420px -780px; } +.emoji-swimmer_tone2 { background-position: -440px -780px; } +.emoji-swimmer_tone3 { background-position: -460px -780px; } +.emoji-swimmer_tone4 { background-position: -480px -780px; } +.emoji-swimmer_tone5 { background-position: -500px -780px; } +.emoji-symbols { background-position: -520px -780px; } +.emoji-synagogue { background-position: -540px -780px; } +.emoji-syringe { background-position: -560px -780px; } +.emoji-taco { background-position: -580px -780px; } +.emoji-tada { background-position: -600px -780px; } +.emoji-tanabata_tree { background-position: -620px -780px; } +.emoji-tangerine { background-position: -640px -780px; } +.emoji-taurus { background-position: -660px -780px; } +.emoji-taxi { background-position: -680px -780px; } +.emoji-tea { background-position: -700px -780px; } +.emoji-telephone { background-position: -720px -780px; } +.emoji-telephone_receiver { background-position: -740px -780px; } +.emoji-telescope { background-position: -760px -780px; } +.emoji-ten { background-position: -780px -780px; } +.emoji-tennis { background-position: -800px 0; } +.emoji-tent { background-position: -800px -20px; } +.emoji-thermometer { background-position: -800px -40px; } +.emoji-thermometer_face { background-position: -800px -60px; } +.emoji-thinking { background-position: -800px -80px; } +.emoji-third_place { background-position: -800px -100px; } +.emoji-thought_balloon { background-position: -800px -120px; } +.emoji-three { background-position: -800px -140px; } +.emoji-thumbsdown { background-position: -800px -160px; } +.emoji-thumbsdown_tone1 { background-position: -800px -180px; } +.emoji-thumbsdown_tone2 { background-position: -800px -200px; } +.emoji-thumbsdown_tone3 { background-position: -800px -220px; } +.emoji-thumbsdown_tone4 { background-position: -800px -240px; } +.emoji-thumbsdown_tone5 { background-position: -800px -260px; } +.emoji-thumbsup { background-position: -800px -280px; } +.emoji-thumbsup_tone1 { background-position: -800px -300px; } +.emoji-thumbsup_tone2 { background-position: -800px -320px; } +.emoji-thumbsup_tone3 { background-position: -800px -340px; } +.emoji-thumbsup_tone4 { background-position: -800px -360px; } +.emoji-thumbsup_tone5 { background-position: -800px -380px; } +.emoji-thunder_cloud_rain { background-position: -800px -400px; } +.emoji-ticket { background-position: -800px -420px; } +.emoji-tickets { background-position: -800px -440px; } +.emoji-tiger { background-position: -800px -460px; } +.emoji-tiger2 { background-position: -800px -480px; } +.emoji-timer { background-position: -800px -500px; } +.emoji-tired_face { background-position: -800px -520px; } +.emoji-tm { background-position: -800px -540px; } +.emoji-toilet { background-position: -800px -560px; } +.emoji-tokyo_tower { background-position: -800px -580px; } +.emoji-tomato { background-position: -800px -600px; } +.emoji-tone1 { background-position: -800px -620px; } +.emoji-tone2 { background-position: -800px -640px; } +.emoji-tone3 { background-position: -800px -660px; } +.emoji-tone4 { background-position: -800px -680px; } +.emoji-tone5 { background-position: -800px -700px; } +.emoji-tongue { background-position: -800px -720px; } +.emoji-tools { background-position: -800px -740px; } +.emoji-top { background-position: -800px -760px; } +.emoji-tophat { background-position: -800px -780px; } +.emoji-track_next { background-position: 0 -800px; } +.emoji-track_previous { background-position: -20px -800px; } +.emoji-trackball { background-position: -40px -800px; } +.emoji-tractor { background-position: -60px -800px; } +.emoji-traffic_light { background-position: -80px -800px; } +.emoji-train { background-position: -100px -800px; } +.emoji-train2 { background-position: -120px -800px; } +.emoji-tram { background-position: -140px -800px; } +.emoji-triangular_flag_on_post { background-position: -160px -800px; } +.emoji-triangular_ruler { background-position: -180px -800px; } +.emoji-trident { background-position: -200px -800px; } +.emoji-triumph { background-position: -220px -800px; } +.emoji-trolleybus { background-position: -240px -800px; } +.emoji-trophy { background-position: -260px -800px; } +.emoji-tropical_drink { background-position: -280px -800px; } +.emoji-tropical_fish { background-position: -300px -800px; } +.emoji-truck { background-position: -320px -800px; } +.emoji-trumpet { background-position: -340px -800px; } +.emoji-tulip { background-position: -360px -800px; } +.emoji-tumbler_glass { background-position: -380px -800px; } +.emoji-turkey { background-position: -400px -800px; } +.emoji-turtle { background-position: -420px -800px; } +.emoji-tv { background-position: -440px -800px; } +.emoji-twisted_rightwards_arrows { background-position: -460px -800px; } +.emoji-two { background-position: -480px -800px; } +.emoji-two_hearts { background-position: -500px -800px; } +.emoji-two_men_holding_hands { background-position: -520px -800px; } +.emoji-two_women_holding_hands { background-position: -540px -800px; } +.emoji-u5272 { background-position: -560px -800px; } +.emoji-u5408 { background-position: -580px -800px; } +.emoji-u55b6 { background-position: -600px -800px; } +.emoji-u6307 { background-position: -620px -800px; } +.emoji-u6708 { background-position: -640px -800px; } +.emoji-u6709 { background-position: -660px -800px; } +.emoji-u6e80 { background-position: -680px -800px; } +.emoji-u7121 { background-position: -700px -800px; } +.emoji-u7533 { background-position: -720px -800px; } +.emoji-u7981 { background-position: -740px -800px; } +.emoji-u7a7a { background-position: -760px -800px; } +.emoji-umbrella { background-position: -780px -800px; } +.emoji-umbrella2 { background-position: -800px -800px; } +.emoji-unamused { background-position: -820px 0; } +.emoji-underage { background-position: -820px -20px; } +.emoji-unicorn { background-position: -820px -40px; } +.emoji-unlock { background-position: -820px -60px; } +.emoji-up { background-position: -820px -80px; } +.emoji-upside_down { background-position: -820px -100px; } +.emoji-urn { background-position: -820px -120px; } +.emoji-v { background-position: -820px -140px; } +.emoji-v_tone1 { background-position: -820px -160px; } +.emoji-v_tone2 { background-position: -820px -180px; } +.emoji-v_tone3 { background-position: -820px -200px; } +.emoji-v_tone4 { background-position: -820px -220px; } +.emoji-v_tone5 { background-position: -820px -240px; } +.emoji-vertical_traffic_light { background-position: -820px -260px; } +.emoji-vhs { background-position: -820px -280px; } +.emoji-vibration_mode { background-position: -820px -300px; } +.emoji-video_camera { background-position: -820px -320px; } +.emoji-video_game { background-position: -820px -340px; } +.emoji-violin { background-position: -820px -360px; } +.emoji-virgo { background-position: -820px -380px; } +.emoji-volcano { background-position: -820px -400px; } +.emoji-volleyball { background-position: -820px -420px; } +.emoji-vs { background-position: -820px -440px; } +.emoji-vulcan { background-position: -820px -460px; } +.emoji-vulcan_tone1 { background-position: -820px -480px; } +.emoji-vulcan_tone2 { background-position: -820px -500px; } +.emoji-vulcan_tone3 { background-position: -820px -520px; } +.emoji-vulcan_tone4 { background-position: -820px -540px; } +.emoji-vulcan_tone5 { background-position: -820px -560px; } +.emoji-walking { background-position: -820px -580px; } +.emoji-walking_tone1 { background-position: -820px -600px; } +.emoji-walking_tone2 { background-position: -820px -620px; } +.emoji-walking_tone3 { background-position: -820px -640px; } +.emoji-walking_tone4 { background-position: -820px -660px; } +.emoji-walking_tone5 { background-position: -820px -680px; } +.emoji-waning_crescent_moon { background-position: -820px -700px; } +.emoji-waning_gibbous_moon { background-position: -820px -720px; } +.emoji-warning { background-position: -820px -740px; } +.emoji-wastebasket { background-position: -820px -760px; } +.emoji-watch { background-position: -820px -780px; } +.emoji-water_buffalo { background-position: -820px -800px; } +.emoji-water_polo { background-position: 0 -820px; } +.emoji-water_polo_tone1 { background-position: -20px -820px; } +.emoji-water_polo_tone2 { background-position: -40px -820px; } +.emoji-water_polo_tone3 { background-position: -60px -820px; } +.emoji-water_polo_tone4 { background-position: -80px -820px; } +.emoji-water_polo_tone5 { background-position: -100px -820px; } +.emoji-watermelon { background-position: -120px -820px; } +.emoji-wave { background-position: -140px -820px; } +.emoji-wave_tone1 { background-position: -160px -820px; } +.emoji-wave_tone2 { background-position: -180px -820px; } +.emoji-wave_tone3 { background-position: -200px -820px; } +.emoji-wave_tone4 { background-position: -220px -820px; } +.emoji-wave_tone5 { background-position: -240px -820px; } +.emoji-wavy_dash { background-position: -260px -820px; } +.emoji-waxing_crescent_moon { background-position: -280px -820px; } +.emoji-waxing_gibbous_moon { background-position: -300px -820px; } +.emoji-wc { background-position: -320px -820px; } +.emoji-weary { background-position: -340px -820px; } +.emoji-wedding { background-position: -360px -820px; } +.emoji-whale { background-position: -380px -820px; } +.emoji-whale2 { background-position: -400px -820px; } +.emoji-wheel_of_dharma { background-position: -420px -820px; } +.emoji-wheelchair { background-position: -440px -820px; } +.emoji-white_check_mark { background-position: -460px -820px; } +.emoji-white_circle { background-position: -480px -820px; } +.emoji-white_flower { background-position: -500px -820px; } +.emoji-white_large_square { background-position: -520px -820px; } +.emoji-white_medium_small_square { background-position: -540px -820px; } +.emoji-white_medium_square { background-position: -560px -820px; } +.emoji-white_small_square { background-position: -580px -820px; } +.emoji-white_square_button { background-position: -600px -820px; } +.emoji-white_sun_cloud { background-position: -620px -820px; } +.emoji-white_sun_rain_cloud { background-position: -640px -820px; } +.emoji-white_sun_small_cloud { background-position: -660px -820px; } +.emoji-wilted_rose { background-position: -680px -820px; } +.emoji-wind_blowing_face { background-position: -700px -820px; } +.emoji-wind_chime { background-position: -720px -820px; } +.emoji-wine_glass { background-position: -740px -820px; } +.emoji-wink { background-position: -760px -820px; } +.emoji-wolf { background-position: -780px -820px; } +.emoji-woman { background-position: -800px -820px; } +.emoji-woman_tone1 { background-position: -820px -820px; } +.emoji-woman_tone2 { background-position: -840px 0; } +.emoji-woman_tone3 { background-position: -840px -20px; } +.emoji-woman_tone4 { background-position: -840px -40px; } +.emoji-woman_tone5 { background-position: -840px -60px; } +.emoji-womans_clothes { background-position: -840px -80px; } +.emoji-womans_hat { background-position: -840px -100px; } +.emoji-womens { background-position: -840px -120px; } +.emoji-worried { background-position: -840px -140px; } +.emoji-wrench { background-position: -840px -160px; } +.emoji-wrestlers { background-position: -840px -180px; } +.emoji-wrestlers_tone1 { background-position: -840px -200px; } +.emoji-wrestlers_tone2 { background-position: -840px -220px; } +.emoji-wrestlers_tone3 { background-position: -840px -240px; } +.emoji-wrestlers_tone4 { background-position: -840px -260px; } +.emoji-wrestlers_tone5 { background-position: -840px -280px; } +.emoji-writing_hand { background-position: -840px -300px; } +.emoji-writing_hand_tone1 { background-position: -840px -320px; } +.emoji-writing_hand_tone2 { background-position: -840px -340px; } +.emoji-writing_hand_tone3 { background-position: -840px -360px; } +.emoji-writing_hand_tone4 { background-position: -840px -380px; } +.emoji-writing_hand_tone5 { background-position: -840px -400px; } +.emoji-x { background-position: -840px -420px; } +.emoji-yellow_heart { background-position: -840px -440px; } +.emoji-yen { background-position: -840px -460px; } +.emoji-yin_yang { background-position: -840px -480px; } +.emoji-yum { background-position: -840px -500px; } +.emoji-zap { background-position: -840px -520px; } +.emoji-zero { background-position: -840px -540px; } +.emoji-zipper_mouth { background-position: -840px -560px; } +.emoji-100 { background-position: -840px -580px; } .emoji-icon { background-image: image-url('emoji.png'); 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/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/tree.scss b/app/assets/stylesheets/pages/tree.scss index 50f0ef4414a..65b334662c2 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -125,7 +125,7 @@ color: $white-normal; } - &:hover { + &:hover:not(.tree-truncated-warning) { td { background-color: $row-hover; border-top: 1px solid $row-hover-border; @@ -198,6 +198,11 @@ } } + .tree-truncated-warning { + color: $orange-600; + background-color: $orange-100; + } + .tree-time-ago { min-width: 135px; color: $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/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/tree_helper.rb b/app/helpers/tree_helper.rb index c4ea0f5ac53..5b2ea38a03d 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -1,14 +1,23 @@ module TreeHelper + FILE_LIMIT = 1_000 + # Sorts a repository's tree so that folders are before files and renders # their corresponding partials # - # contents - A Grit::Tree object for the current tree + # tree - A `Tree` object for the current tree def render_tree(tree) # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = "" + tree = '' items = (folders + submodules).sort_by(&:name) + files - tree << render(partial: "projects/tree/tree_row", collection: items) if items.present? + + if items.size > FILE_LIMIT + tree << render(partial: 'projects/tree/truncated_notice_tree_row', + locals: { limit: FILE_LIMIT, total: items.size }) + items = items.take(FILE_LIMIT) + end + + tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? tree.html_safe end @@ -88,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/concerns/issuable.rb b/app/models/concerns/issuable.rb index c008fb91a16..35090181bd9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -255,7 +255,7 @@ module Issuable participants(user).include?(user) end - def to_hook_data(user, old_labels: [], old_assignees: []) + def to_hook_data(user, old_labels: [], old_assignees: [], old_total_time_spent: nil) changes = previous_changes if old_labels != labels @@ -270,6 +270,10 @@ module Issuable end end + if old_total_time_spent != total_time_spent + changes[:total_time_spent] = [old_total_time_spent, total_time_spent] + end + Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes) 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 43de6809178..a0af749a93f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -21,7 +21,7 @@ class ProjectWiki end delegate :empty?, to: :pages - delegate :repository_storage_path, to: :project + delegate :repository_storage_path, :hashed_storage?, to: :project def path @project.path + '.wiki' @@ -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..26d1bc12232 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 @@ -242,6 +246,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 +478,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 +672,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 +1001,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/issuable_base_service.rb b/app/services/issuable_base_service.rb index 90865867ff0..39a7299ff60 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -172,6 +172,7 @@ class IssuableBaseService < BaseService old_labels = issuable.labels.to_a old_mentioned_users = issuable.mentioned_users.to_a old_assignees = issuable.assignees.to_a + old_total_time_spent = issuable.total_time_spent label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) @@ -208,7 +209,12 @@ class IssuableBaseService < BaseService invalidate_cache_counts(issuable, users: affected_assignees.compact) after_update(issuable) issuable.create_new_cross_references!(current_user) - execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees) + execute_hooks( + issuable, + 'update', + old_labels: old_labels, + old_assignees: old_assignees, + old_total_time_spent: old_total_time_spent) issuable.update_project_counter_caches if update_project_counters end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index b680eaf5a49..0f711bcc3cf 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,7 +1,7 @@ module Issues class BaseService < ::IssuableBaseService - def hook_data(issue, action, old_labels: [], old_assignees: []) - hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees) + def hook_data(issue, action, old_labels: [], old_assignees: [], old_total_time_spent: nil) + hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent) hook_data[:object_attributes][:action] = action hook_data @@ -22,8 +22,8 @@ module Issues issue, issue.project, current_user, old_assignees) end - def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: []) - issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees) + def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [], old_total_time_spent: nil) + issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent) hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks issue.project.execute_hooks(issue_data, hooks_scope) issue.project.execute_services(issue_data, hooks_scope) 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/base_service.rb b/app/services/merge_requests/base_service.rb index 112606a82d7..d3938b065bc 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -18,8 +18,8 @@ module MergeRequests super if changed_title end - def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: []) - hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees) + def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil) + hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent) hook_data[:object_attributes][:action] = action if old_rev && !Gitlab::Git.blank_ref?(old_rev) hook_data[:object_attributes][:oldrev] = old_rev @@ -28,9 +28,9 @@ module MergeRequests hook_data end - def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: []) + def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil) if merge_request.project - merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees) + merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent) merge_request.project.execute_hooks(merge_data, :merge_request_hooks) merge_request.project.execute_services(merge_data, :merge_request_hooks) 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/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/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/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 1d6a0fa38ca..36b28c731a1 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: '#', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } + %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index b6b7aae6f9a..44aa8002f12 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@ = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%a{ href: '#', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } +%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } = label 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/tree/_truncated_notice_tree_row.html.haml b/app/views/projects/tree/_truncated_notice_tree_row.html.haml new file mode 100644 index 00000000000..693b641888b --- /dev/null +++ b/app/views/projects/tree/_truncated_notice_tree_row.html.haml @@ -0,0 +1,7 @@ +%tr.tree-truncated-warning + %td{ colspan: '3' } + = icon('exclamation-triangle fw') + %span + Too many items to show. To preserve performance only + %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)} + items are displayed. 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/_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 diff --git a/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml new file mode 100644 index 00000000000..e3c7ffc8046 --- /dev/null +++ b/changelogs/unreleased/18040-rubocop-line-break-after-guard-clause.yml @@ -0,0 +1,5 @@ +--- +title: Adds Rubocop rule for line break after guard clause +merge_request: 15188 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml new file mode 100644 index 00000000000..b217cb44bf7 --- /dev/null +++ b/changelogs/unreleased/1870-impersonation-stuck-on-password-change.yml @@ -0,0 +1,5 @@ +--- +title: Impersonation no longer gets stuck on password change. +merge_request: 15497 +author: +type: fixed diff --git a/changelogs/unreleased/34600-performance-wiki-pages.yml b/changelogs/unreleased/34600-performance-wiki-pages.yml new file mode 100644 index 00000000000..541ae8f8e60 --- /dev/null +++ b/changelogs/unreleased/34600-performance-wiki-pages.yml @@ -0,0 +1,5 @@ +--- +title: Performance issues when loading large number of wiki pages +merge_request: 15276 +author: +type: performance diff --git a/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml new file mode 100644 index 00000000000..c73cf8bf60b --- /dev/null +++ b/changelogs/unreleased/38393-Milestone-duration-error-message-is-not-accurate-enough.yml @@ -0,0 +1,5 @@ +--- +title: Changed validation error message on wrong milestone dates +merge_request: +author: Xurxo Méndez Pérez +type: fixed diff --git a/changelogs/unreleased/38822-oauth-search-case-insensitive.yml b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml new file mode 100644 index 00000000000..d84360b4c5c --- /dev/null +++ b/changelogs/unreleased/38822-oauth-search-case-insensitive.yml @@ -0,0 +1,5 @@ +--- +title: OAuth identity lookups case-insensitive +merge_request: 15312 +author: +type: fixed diff --git a/changelogs/unreleased/39167-async-boards-sidebar.yml b/changelogs/unreleased/39167-async-boards-sidebar.yml new file mode 100644 index 00000000000..dc77f1ad451 --- /dev/null +++ b/changelogs/unreleased/39167-async-boards-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Update Issue Boards to fetch the notification subscription status asynchronously +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml new file mode 100644 index 00000000000..36c2f789eeb --- /dev/null +++ b/changelogs/unreleased/39461-notes-api-for-issues-no-longer-returns-label-additions-removals.yml @@ -0,0 +1,5 @@ +--- +title: Label addition/removal are not going to be redacted wrongfully in the API. +merge_request: 15080 +author: +type: fixed diff --git a/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml new file mode 100644 index 00000000000..fc7c024f95a --- /dev/null +++ b/changelogs/unreleased/39497-inline-edit-issue-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Add inline editing to issues on mobile +merge_request: 15438 +author: +type: changed diff --git a/changelogs/unreleased/39573-hashed-storage-backup.yml b/changelogs/unreleased/39573-hashed-storage-backup.yml new file mode 100644 index 00000000000..40ee589c8cc --- /dev/null +++ b/changelogs/unreleased/39573-hashed-storage-backup.yml @@ -0,0 +1,5 @@ +--- +title: Fix gitlab:backup rake for hashed storage based repositories +merge_request: 15400 +author: +type: fixed diff --git a/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml new file mode 100644 index 00000000000..8b27c43d15b --- /dev/null +++ b/changelogs/unreleased/39821-fix-commits-list-with-multi-file-editor.yml @@ -0,0 +1,5 @@ +--- +title: Fix commits page throwing 500 when the multi-file editor was enabled +merge_request: 15502 +author: +type: fixed diff --git a/changelogs/unreleased/40016-log-header.yml b/changelogs/unreleased/40016-log-header.yml new file mode 100644 index 00000000000..f52c2d2a0d5 --- /dev/null +++ b/changelogs/unreleased/40016-log-header.yml @@ -0,0 +1,5 @@ +--- +title: Hide log size for mobile screens +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml new file mode 100644 index 00000000000..a2ae2059c47 --- /dev/null +++ b/changelogs/unreleased/40122-only-one-note-webhook-is-triggered-when-a-comment-with-time-spent-is-added.yml @@ -0,0 +1,5 @@ +--- +title: Add total_time_spent to the `changes` hash in issuable Webhook payloads +merge_request: 15381 +author: +type: changed diff --git a/changelogs/unreleased/40198-fix-gpg-badge-links.yml b/changelogs/unreleased/40198-fix-gpg-badge-links.yml new file mode 100644 index 00000000000..62b962acefa --- /dev/null +++ b/changelogs/unreleased/40198-fix-gpg-badge-links.yml @@ -0,0 +1,6 @@ +--- +title: Fix issue where clicking a GPG verification badge would scroll to the top of + the page +merge_request: 15407 +author: +type: fixed diff --git a/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml new file mode 100644 index 00000000000..9c308321a19 --- /dev/null +++ b/changelogs/unreleased/40290-remove-rake-gitlab-sidekiq-drop-post-receive.yml @@ -0,0 +1,5 @@ +--- +title: Removed unused rake task, 'rake gitlab:sidekiq:drop_post_receive' +merge_request: 15493 +author: +type: fixed diff --git a/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml new file mode 100644 index 00000000000..e5879f89156 --- /dev/null +++ b/changelogs/unreleased/40292-bitbucket-import-hashed-storage.yml @@ -0,0 +1,5 @@ +--- +title: Fix bitbucket wiki import with hashed storage enabled +merge_request: 15490 +author: +type: fixed diff --git a/changelogs/unreleased/40377-blank-states.yml b/changelogs/unreleased/40377-blank-states.yml new file mode 100644 index 00000000000..7635602c68c --- /dev/null +++ b/changelogs/unreleased/40377-blank-states.yml @@ -0,0 +1,5 @@ +--- +title: Fix blank states using old css +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/4080-align-retry-btn.yml b/changelogs/unreleased/4080-align-retry-btn.yml new file mode 100644 index 00000000000..c7d3997839c --- /dev/null +++ b/changelogs/unreleased/4080-align-retry-btn.yml @@ -0,0 +1,5 @@ +--- +title: Align retry button with job title with new grid size +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/brand_header_change.yml b/changelogs/unreleased/brand_header_change.yml new file mode 100644 index 00000000000..6ea6e8192a4 --- /dev/null +++ b/changelogs/unreleased/brand_header_change.yml @@ -0,0 +1,5 @@ +--- +title: When a custom header logo is present, don't show GitLab type logo +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/bvl-delete-empty-fork-networks.yml b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml new file mode 100644 index 00000000000..3bbb4cf6e3c --- /dev/null +++ b/changelogs/unreleased/bvl-delete-empty-fork-networks.yml @@ -0,0 +1,5 @@ +--- +title: Clean up empty fork networks +merge_request: 15373 +author: +type: other diff --git a/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml new file mode 100644 index 00000000000..e0895cb5d48 --- /dev/null +++ b/changelogs/unreleased/bvl-dont-move-projects-using-hashed-storage.yml @@ -0,0 +1,5 @@ +--- +title: Don't move repositories and attachments for projects using hashed storage +merge_request: 15479 +author: +type: other diff --git a/changelogs/unreleased/bvl-fix-count-with-selects.yml b/changelogs/unreleased/bvl-fix-count-with-selects.yml new file mode 100644 index 00000000000..46a882de524 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-count-with-selects.yml @@ -0,0 +1,6 @@ +--- +title: Fix crash when navigating to second page of the group dashbaord when there + are projects and groups on the first page +merge_request: 15456 +author: +type: fixed diff --git a/changelogs/unreleased/cache-user-keys-count.yml b/changelogs/unreleased/cache-user-keys-count.yml new file mode 100644 index 00000000000..181be95622c --- /dev/null +++ b/changelogs/unreleased/cache-user-keys-count.yml @@ -0,0 +1,5 @@ +--- +title: Cache the number of user SSH keys +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/ci-pipeline-status-query.yml b/changelogs/unreleased/ci-pipeline-status-query.yml new file mode 100644 index 00000000000..a464e501418 --- /dev/null +++ b/changelogs/unreleased/ci-pipeline-status-query.yml @@ -0,0 +1,5 @@ +--- +title: Optimise getting the pipeline status of commits +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/dm-notes-actions-noteable-for-update.yml b/changelogs/unreleased/dm-notes-actions-noteable-for-update.yml new file mode 100644 index 00000000000..1d2f58bc765 --- /dev/null +++ b/changelogs/unreleased/dm-notes-actions-noteable-for-update.yml @@ -0,0 +1,5 @@ +--- +title: Make sure NotesActions#noteable returns a Noteable in the update action +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-gb-update-registry-path-reference-regexp.yml b/changelogs/unreleased/fix-gb-update-registry-path-reference-regexp.yml new file mode 100644 index 00000000000..55c1089ade5 --- /dev/null +++ b/changelogs/unreleased/fix-gb-update-registry-path-reference-regexp.yml @@ -0,0 +1,5 @@ +--- +title: Update container repository path reference and allow using double underscore +merge_request: 15417 +author: +type: fixed diff --git a/changelogs/unreleased/fix-protected-branches-descriptions.yml b/changelogs/unreleased/fix-protected-branches-descriptions.yml new file mode 100644 index 00000000000..8e233d9defd --- /dev/null +++ b/changelogs/unreleased/fix-protected-branches-descriptions.yml @@ -0,0 +1,5 @@ +--- +title: Clarify wording of protected branch settings for the default branch +merge_request: +author: +type: other diff --git a/changelogs/unreleased/improved-changes-dropdown.yml b/changelogs/unreleased/improved-changes-dropdown.yml new file mode 100644 index 00000000000..f305cbe573b --- /dev/null +++ b/changelogs/unreleased/improved-changes-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Improved diff changed files dropdown design +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/issue_40337.yml b/changelogs/unreleased/issue_40337.yml new file mode 100644 index 00000000000..0cd6c5f46a9 --- /dev/null +++ b/changelogs/unreleased/issue_40337.yml @@ -0,0 +1,5 @@ +--- +title: Fix promoting milestone updating all issuables without milestone +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/merge-requests-schema-cleanup.yml b/changelogs/unreleased/merge-requests-schema-cleanup.yml new file mode 100644 index 00000000000..ccce9b1436c --- /dev/null +++ b/changelogs/unreleased/merge-requests-schema-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Clean up schema of the "merge_requests" table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/mk-add-user-rate-limits.yml b/changelogs/unreleased/mk-add-user-rate-limits.yml new file mode 100644 index 00000000000..512757da5fc --- /dev/null +++ b/changelogs/unreleased/mk-add-user-rate-limits.yml @@ -0,0 +1,6 @@ +--- +title: Add anonymous rate limit per IP, and authenticated (web or API) rate limits + per user +merge_request: 14708 +author: +type: added diff --git a/changelogs/unreleased/osw-merge-process-logs.yml b/changelogs/unreleased/osw-merge-process-logs.yml new file mode 100644 index 00000000000..d2bb0e09834 --- /dev/null +++ b/changelogs/unreleased/osw-merge-process-logs.yml @@ -0,0 +1,5 @@ +--- +title: Add logs for monitoring the merge process +merge_request: +author: +type: other diff --git a/changelogs/unreleased/reduce-queries-for-artifacts-button.yml b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml new file mode 100644 index 00000000000..f2d469b5a80 --- /dev/null +++ b/changelogs/unreleased/reduce-queries-for-artifacts-button.yml @@ -0,0 +1,5 @@ +--- +title: Use arrays in Pipeline#latest_builds_with_artifacts +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-port-hashed-storage-transfer-fix.yml b/changelogs/unreleased/sh-port-hashed-storage-transfer-fix.yml new file mode 100644 index 00000000000..c32afc90f64 --- /dev/null +++ b/changelogs/unreleased/sh-port-hashed-storage-transfer-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix hashed storage with project transfers to another namespace +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/skip_confirmation_user_API.yml b/changelogs/unreleased/skip_confirmation_user_API.yml new file mode 100644 index 00000000000..144ccd69e68 --- /dev/null +++ b/changelogs/unreleased/skip_confirmation_user_API.yml @@ -0,0 +1,7 @@ +--- +title: Add email confirmation parameters for user creation and update via API +merge_request: +author: Daniel Juarez +type: added + + diff --git a/changelogs/unreleased/tree_item_limit.yml b/changelogs/unreleased/tree_item_limit.yml new file mode 100644 index 00000000000..d95c5776075 --- /dev/null +++ b/changelogs/unreleased/tree_item_limit.yml @@ -0,0 +1,5 @@ +--- +title: Truncate tree to max 1,000 items and display notice to users +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml new file mode 100644 index 00000000000..e509a8df6bc --- /dev/null +++ b/changelogs/unreleased/update-emoji-digests-with-latest-from-gemojione.yml @@ -0,0 +1,6 @@ +--- +title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma + in :cartwheel_tone4:' +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/zj-commit-show-n-1.yml b/changelogs/unreleased/zj-commit-show-n-1.yml new file mode 100644 index 00000000000..e536434f74a --- /dev/null +++ b/changelogs/unreleased/zj-commit-show-n-1.yml @@ -0,0 +1,5 @@ +--- +title: Fetch blobs in bulk when generating diffs +merge_request: +author: +type: performance diff --git a/config/application.rb b/config/application.rb index 5100ec5d2b7..6436f887d14 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,7 +113,7 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) - config.middleware.insert_before Warden::Manager, Rack::Attack + config.middleware.insert_after Warden::Manager, Rack::Attack # Allow access to GitLab API from other domains config.middleware.insert_before Warden::Manager, Rack::Cors do diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 3af7f7bd5c0..60df92a44fc 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -459,9 +459,9 @@ :versions: [] :when: 2017-09-13 17:31:16.425819400 Z - - :approve - - gitlab-svgs + - "@gitlab-org/gitlab-svgs" - :who: Tim Zallmann - :why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs + :why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs :versions: [] :when: 2017-09-19 14:36:32.795496000 Z - - :license @@ -471,3 +471,35 @@ :why: :versions: [] :when: 2017-10-17 17:46:12.367554000 Z +- - :license + - component-emitter + - MIT + - :who: Winnie Hellmann + :why: package.json does not specify the license (README.md does) + :versions: + - 1.1.2 + :when: 2017-11-13 12:23:10.502463000 Z +- - :license + - json-schema + - BSD + - :who: Winnie Hellmann + :why: https://github.com/kriszyp/json-schema/blob/v0.2.3/package.json#L18-L19 + :versions: + - 0.2.3 + :when: 2017-11-16 12:52:18.286091000 Z +- - :license + - node-forge + - New BSD + - :who: Winnie Hellmann + :why: https://github.com/digitalbazaar/forge/blob/0.6.33/LICENSE + :versions: + - 0.6.33 + :when: 2017-11-16 12:56:17.974767000 Z +- - :license + - sntp + - BSD + - :who: Winnie Hellmann + :why: https://github.com/hueniverse/sntp/blob/v1.0.9/package.json#L28-L29 + :versions: + - 1.0.9 + :when: 2017-11-16 13:02:06.765282000 Z diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb index 35e8b3808e2..6ebaf8834d2 100644 --- a/config/initializers/ar5_batching.rb +++ b/config/initializers/ar5_batching.rb @@ -34,6 +34,7 @@ module ActiveRecord yield yielded_relation break if ids.length < of + batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset)) end end diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb new file mode 100644 index 00000000000..2e2256b0eb9 --- /dev/null +++ b/config/initializers/batch_loader.rb @@ -0,0 +1 @@ +Rails.application.config.middleware.use(BatchLoader::Middleware) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 958859be6cf..051ef93b205 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -236,6 +236,7 @@ Devise.setup do |config| provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) + Gitlab::OAuth::Session.destroy(:cas3, ticket) true end diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index 1ebe3c7a742..2fd47a3f4d3 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -10,4 +10,32 @@ module Gollum index.send(name, *args) end end + + class Wiki + def pages(treeish = nil, limit: nil) + tree_list((treeish || @ref), limit: limit) + end + + def tree_list(ref, limit: nil) + if (sha = @access.ref_to_sha(ref)) + commit = @access.commit(sha) + tree_map_for(sha).inject([]) do |list, entry| + next list unless @page_class.valid_page_name?(entry.name) + + list << entry.page(self, commit) + break list if limit && list.size >= limit + + list + end + else + [] + end + end + end +end + +Rails.application.configure do + config.after_initialize do + Gollum::Page.per_page = Kaminari.config.default_per_page + end end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index fddb018e948..e9e1f1c4e9b 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -3,6 +3,7 @@ if Gitlab::LDAP::Config.enabled? Gitlab::LDAP::Config.available_servers.each do |server| # do not redeclare LDAP next if server['provider_name'] == 'ldap' + const_set(server['provider_class'], Class.new(LDAP)) end end diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb index 7f0df8949db..38a9cd68d57 100644 --- a/config/initializers/postgresql_cte.rb +++ b/config/initializers/postgresql_cte.rb @@ -61,11 +61,13 @@ module ActiveRecord def with_values=(values) raise ImmutableRelation if @loaded + @values[:with] = values end def recursive_value=(value) raise ImmutableRelation if @loaded + @values[:recursive] = value end diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb new file mode 100644 index 00000000000..9453df2ec5a --- /dev/null +++ b/config/initializers/rack_attack_global.rb @@ -0,0 +1,61 @@ +module Gitlab::Throttle + def self.settings + Gitlab::CurrentSettings.current_application_settings + end + + def self.unauthenticated_options + limit_proc = proc { |req| settings.throttle_unauthenticated_requests_per_period } + period_proc = proc { |req| settings.throttle_unauthenticated_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_api_options + limit_proc = proc { |req| settings.throttle_authenticated_api_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_api_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end + + def self.authenticated_web_options + limit_proc = proc { |req| settings.throttle_authenticated_web_requests_per_period } + period_proc = proc { |req| settings.throttle_authenticated_web_period_in_seconds.seconds } + { limit: limit_proc, period: period_proc } + end +end + +class Rack::Attack + throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| + Gitlab::Throttle.settings.throttle_unauthenticated_enabled && + req.unauthenticated? && + req.ip + end + + throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req| + Gitlab::Throttle.settings.throttle_authenticated_api_enabled && + req.api_request? && + req.authenticated_user_id + end + + throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req| + Gitlab::Throttle.settings.throttle_authenticated_web_enabled && + req.web_request? && + req.authenticated_user_id + end + + class Request + def unauthenticated? + !authenticated_user_id + end + + def authenticated_user_id + Gitlab::Auth::RequestAuthenticator.new(self).user&.id + end + + def api_request? + path.start_with?('/api') + end + + def web_request? + !api_request? + end + end +end diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 383782112a8..96c6d954ff7 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -4,7 +4,7 @@ require './spec/support/test_env' class Gitlab::Seeder::CycleAnalytics def initialize(project, perf: false) @project = project - @user = User.order(:id).last + @user = User.admins.first @issue_count = perf ? 1000 : 5 stub_git_pre_receive! end @@ -77,39 +77,41 @@ class Gitlab::Seeder::CycleAnalytics end def seed! - Sidekiq::Testing.inline! do - issues = create_issues - puts '.' - - # Stage 1 - Timecop.travel 5.days.from_now - add_milestones_and_list_labels(issues) - print '.' - - # Stage 2 - Timecop.travel 5.days.from_now - branches = mention_in_commits(issues) - print '.' - - # Stage 3 - Timecop.travel 5.days.from_now - merge_requests = create_merge_requests_closing_issues(issues, branches) - print '.' - - # Stage 4 - Timecop.travel 5.days.from_now - run_builds(merge_requests) - print '.' - - # Stage 5 - Timecop.travel 5.days.from_now - merge_merge_requests(merge_requests) - print '.' - - # Stage 6 / 7 - Timecop.travel 5.days.from_now - deploy_to_production(merge_requests) - print '.' + Sidekiq::Worker.skipping_transaction_check do + Sidekiq::Testing.inline! do + issues = create_issues + puts '.' + + # Stage 1 + Timecop.travel 5.days.from_now + add_milestones_and_list_labels(issues) + print '.' + + # Stage 2 + Timecop.travel 5.days.from_now + branches = mention_in_commits(issues) + print '.' + + # Stage 3 + Timecop.travel 5.days.from_now + merge_requests = create_merge_requests_closing_issues(issues, branches) + print '.' + + # Stage 4 + Timecop.travel 5.days.from_now + run_builds(merge_requests) + print '.' + + # Stage 5 + Timecop.travel 5.days.from_now + merge_merge_requests(merge_requests) + print '.' + + # Stage 6 / 7 + Timecop.travel 5.days.from_now + deploy_to_production(merge_requests) + print '.' + end end print '.' @@ -123,7 +125,7 @@ class Gitlab::Seeder::CycleAnalytics title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}", description: FFaker::Lorem.sentence, state: 'opened', - assignee: @project.team.users.sample + assignees: [@project.team.users.sample] } Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute @@ -155,7 +157,7 @@ class Gitlab::Seeder::CycleAnalytics issue.project.repository.add_branch(@user, branch_name, 'master') - commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name) + commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name) issue.project.repository.commit(commit_sha) GitPushService.new(issue.project, @@ -210,6 +212,8 @@ class Gitlab::Seeder::CycleAnalytics def deploy_to_production(merge_requests) merge_requests.each do |merge_request| + next unless merge_request.head_pipeline + Timecop.travel 12.hours.from_now job = merge_request.head_pipeline.builds.where.not(environment: nil).last @@ -223,7 +227,14 @@ Gitlab::Seeder.quiet do flag = 'SEED_CYCLE_ANALYTICS' if ENV[flag] - Project.all.each do |project| + Project.find_each do |project| + # This seed naively assumes that every project has a repository, and every + # repository has a `master` branch, which may be the case for a pristine + # GDK seed, but is almost never true for a GDK that's actually had + # development performed on it. + next unless project.repository_exists? + next unless project.repository.commit('master') + seeder = Gitlab::Seeder::CycleAnalytics.new(project) seeder.seed! end diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb index 22bac46e25c..1716b6e8153 100644 --- a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb +++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160608195742_add_repository_storage_to_projects.rb b/db/migrate/20160608195742_add_repository_storage_to_projects.rb index 0f3664c13ef..e4febd1614d 100644 --- a/db/migrate/20160608195742_add_repository_storage_to_projects.rb +++ b/db/migrate/20160608195742_add_repository_storage_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRepositoryStorageToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb index 5336b036bca..c58cb957df4 100644 --- a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb +++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class SetMissingStageOnCiBuilds < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb index 5dc26f8982a..22c925799a3 100644 --- a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb +++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRequestAccessEnabledToProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb index 4a317646788..4fcb29e1325 100644 --- a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb +++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddRequestAccessEnabledToGroups < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb index abe8e701e23..58f7f2a2841 100644 --- a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb +++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb index 7414a28ac97..aec709aaf59 100644 --- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb +++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb @@ -1,7 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RemoveFeaturesEnabledFromProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb index 0100e30a733..df7d922b816 100644 --- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb +++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb @@ -1,7 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RemoveProjectsPushesSinceGc < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb index ae37da275fd..27ebe0af33b 100644 --- a/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb +++ b/db/migrate/20170124193147_add_two_factor_columns_to_namespaces.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddTwoFactorColumnsToNamespaces < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb index 8d4aefa4365..558a1837c79 100644 --- a/db/migrate/20170124193205_add_two_factor_columns_to_users.rb +++ b/db/migrate/20170124193205_add_two_factor_columns_to_users.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddTwoFactorColumnsToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb index 7ad01a04815..6d43f346d4f 100644 --- a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb +++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb @@ -1,7 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb index f335e77fb5e..3c5cd95726a 100644 --- a/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb +++ b/db/migrate/20170305180853_add_auto_cancel_pending_pipelines_to_project.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb index 6c9fe19ca34..807dfcb385d 100644 --- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb +++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb @@ -1,4 +1,4 @@ -# rubocop:disable Migration/AddColumnWithDefaultToLargeTable +# rubocop:disable Migration/UpdateLargeTable class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb index 7b61e811317..255b5e9c4db 100644 --- a/db/migrate/20170320173259_migrate_assignees.rb +++ b/db/migrate/20170320173259_migrate_assignees.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class MigrateAssignees < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb index b2009b282e9..8423bf13fd9 100644 --- a/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb +++ b/db/migrate/20170919211300_remove_temporary_ci_builds_index.rb @@ -12,6 +12,7 @@ class RemoveTemporaryCiBuildsIndex < ActiveRecord::Migration def up return unless index_exists?(:ci_builds, :id, name: 'index_for_ci_builds_retried_migration') + remove_concurrent_index(:ci_builds, :id, name: "index_for_ci_builds_retried_migration") end diff --git a/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb new file mode 100644 index 00000000000..55e822752af --- /dev/null +++ b/db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddGlobalRateLimitsToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :throttle_unauthenticated_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_unauthenticated_requests_per_period, :integer, default: 3600, allow_null: false + add_column_with_default :application_settings, :throttle_unauthenticated_period_in_seconds, :integer, default: 3600, allow_null: false + + add_column_with_default :application_settings, :throttle_authenticated_api_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_api_requests_per_period, :integer, default: 7200, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_api_period_in_seconds, :integer, default: 3600, allow_null: false + + add_column_with_default :application_settings, :throttle_authenticated_web_enabled, :boolean, default: false, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_web_requests_per_period, :integer, default: 7200, allow_null: false + add_column_with_default :application_settings, :throttle_authenticated_web_period_in_seconds, :integer, default: 3600, allow_null: false + end + + def down + remove_column :application_settings, :throttle_authenticated_web_period_in_seconds + remove_column :application_settings, :throttle_authenticated_web_requests_per_period + remove_column :application_settings, :throttle_authenticated_web_enabled + + remove_column :application_settings, :throttle_authenticated_api_period_in_seconds + remove_column :application_settings, :throttle_authenticated_api_requests_per_period + remove_column :application_settings, :throttle_authenticated_api_enabled + + remove_column :application_settings, :throttle_unauthenticated_period_in_seconds + remove_column :application_settings, :throttle_unauthenticated_requests_per_period + remove_column :application_settings, :throttle_unauthenticated_enabled + end +end diff --git a/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb new file mode 100644 index 00000000000..021eaa04a0c --- /dev/null +++ b/db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb @@ -0,0 +1,43 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsAuthorIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_authors + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.author_id = users.id)') + .where('author_id IS NOT NULL') + end + end + + def up + # Replacing the ghost user ID logic would be too complex, hence we don't + # redefine the User model here. + ghost_id = User.select(:id).ghost.id + + MergeRequest.with_orphaned_authors.each_batch(of: 100) do |batch| + batch.update_all(author_id: ghost_id) + end + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :author_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:merge_requests, column: :author_id) + end +end diff --git a/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb new file mode 100644 index 00000000000..1a242f01051 --- /dev/null +++ b/db/migrate/20171114160005_merge_requests_assignee_id_foreign_key.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsAssigneeIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_assignees + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.assignee_id = users.id)') + .where('assignee_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_assignees.each_batch(of: 100) do |batch| + batch.update_all(assignee_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :assignee_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key(:merge_requests, column: :assignee_id) + end +end diff --git a/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb new file mode 100644 index 00000000000..eb3872e38da --- /dev/null +++ b/db/migrate/20171114160904_merge_requests_updated_by_id_foreign_key.rb @@ -0,0 +1,46 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsUpdatedByIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_updaters + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.updated_by_id = users.id)') + .where('updated_by_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_updaters.each_batch(of: 100) do |batch| + batch.update_all(updated_by_id: nil) + end + + add_concurrent_index( + :merge_requests, + :updated_by_id, + where: 'updated_by_id IS NOT NULL' + ) + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :updated_by_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :updated_by_id) + remove_concurrent_index(:merge_requests, :updated_by_id) + end +end diff --git a/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb new file mode 100644 index 00000000000..925b3e537d7 --- /dev/null +++ b/db/migrate/20171114161720_merge_requests_merge_user_id_foreign_key.rb @@ -0,0 +1,46 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsMergeUserIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_mergers + where('NOT EXISTS (SELECT true FROM users WHERE merge_requests.merge_user_id = users.id)') + .where('merge_user_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_mergers.each_batch(of: 100) do |batch| + batch.update_all(merge_user_id: nil) + end + + add_concurrent_index( + :merge_requests, + :merge_user_id, + where: 'merge_user_id IS NOT NULL' + ) + + add_concurrent_foreign_key( + :merge_requests, + :users, + column: :merge_user_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :merge_user_id) + remove_concurrent_index(:merge_requests, :merge_user_id) + end +end diff --git a/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb new file mode 100644 index 00000000000..99740f64fe6 --- /dev/null +++ b/db/migrate/20171114161914_merge_requests_source_project_id_foreign_key.rb @@ -0,0 +1,45 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsSourceProjectIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_source_projects + where('NOT EXISTS (SELECT true FROM projects WHERE merge_requests.source_project_id = projects.id)') + .where('source_project_id IS NOT NULL') + end + end + + def up + # We need to allow NULL values so we can nullify the column when the source + # project is removed. We _don't_ want to remove the merge request, instead + # the application will keep them but close them. + change_column_null(:merge_requests, :source_project_id, true) + + MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch| + batch.update_all(source_project_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :projects, + column: :source_project_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :source_project_id) + change_column_null(:merge_requests, :source_project_id, false) + end +end diff --git a/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb new file mode 100644 index 00000000000..c005cf7d173 --- /dev/null +++ b/db/migrate/20171114162227_merge_requests_milestone_id_foreign_key.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MergeRequestsMilestoneIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.with_orphaned_milestones + where('NOT EXISTS (SELECT true FROM milestones WHERE merge_requests.milestone_id = milestones.id)') + .where('milestone_id IS NOT NULL') + end + end + + def up + MergeRequest.with_orphaned_milestones.each_batch(of: 100) do |batch| + batch.update_all(milestone_id: nil) + end + + add_concurrent_foreign_key( + :merge_requests, + :milestones, + column: :milestone_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:merge_requests, column: :milestone_id) + end +end diff --git a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb index 82f8147547e..f1f81691f81 100644 --- a/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb +++ b/db/post_migrate/20170131214021_reset_users_authorized_projects_populated.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class ResetUsersAuthorizedProjectsPopulated < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb index 01fff680183..49fd46b0262 100644 --- a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class ResetRelativePositionForIssue < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb index cb1b4f1855d..78413a608f1 100644 --- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb +++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb index f2ce25d4524..0ab3d61730d 100644 --- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb +++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb @@ -14,6 +14,7 @@ class CleanUploadSymlinks < ActiveRecord::Migration DIRECTORIES_TO_MOVE.each do |dir| symlink_location = File.join(old_upload_dir, dir) next unless File.symlink?(symlink_location) + say "removing symlink: #{symlink_location}" FileUtils.rm(symlink_location) end diff --git a/db/post_migrate/20170406142253_migrate_user_project_view.rb b/db/post_migrate/20170406142253_migrate_user_project_view.rb index c4e910b3b44..d6061dd416d 100644 --- a/db/post_migrate/20170406142253_migrate_user_project_view.rb +++ b/db/post_migrate/20170406142253_migrate_user_project_view.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index 765daa0a347..bba37e32c01 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # rubocop:disable Migration/UpdateColumnInBatches class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index 9d9f36550e7..b0b58ab3011 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateRetriedForCiBuild < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb index f77078ddd70..81e9d050668 100644 --- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb +++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb index c78beda9d21..3e952980866 100644 --- a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb +++ b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb index 97cb242415d..31a73bb3b27 100644 --- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb +++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class MigrateBuildStageReferenceAgain < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb index 2b79a87ccd8..c735dc67f44 100644 --- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb +++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb @@ -32,6 +32,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration file_name = upload['path'].split('/')[1] next unless move_file(upload['model_id'], secret, file_name) + update_markdown(upload['model_id'], secret, file_name, upload['description']) end end diff --git a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb index acb895e426f..17849b78ceb 100644 --- a/db/post_migrate/20170613111224_clean_appearance_symlinks.rb +++ b/db/post_migrate/20170613111224_clean_appearance_symlinks.rb @@ -13,6 +13,7 @@ class CleanAppearanceSymlinks < ActiveRecord::Migration symlink_location = File.join(old_upload_dir, dir) return unless File.symlink?(symlink_location) + say "removing symlink: #{symlink_location}" FileUtils.rm(symlink_location) end diff --git a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb index a238216253b..b040c81b316 100644 --- a/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb +++ b/db/post_migrate/20170927112318_update_legacy_diff_notes_type_for_import.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170927112319_update_notes_type_for_import.rb b/db/post_migrate/20170927112319_update_notes_type_for_import.rb index 1e70acd9868..5a400c71b02 100644 --- a/db/post_migrate/20170927112319_update_notes_type_for_import.rb +++ b/db/post_migrate/20170927112319_update_notes_type_for_import.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/UpdateLargeTable class UpdateNotesTypeForImport < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb deleted file mode 100644 index a7ebbbf34c0..00000000000 --- a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb +++ /dev/null @@ -1,27 +0,0 @@ -class PopulateMergeRequestsLatestMergeRequestDiffId < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - BATCH_SIZE = 1_000 - - class MergeRequest < ActiveRecord::Base - self.table_name = 'merge_requests' - - include ::EachBatch - end - - disable_ddl_transaction! - - def up - update = ' - latest_merge_request_diff_id = ( - SELECT MAX(id) - FROM merge_request_diffs - WHERE merge_requests.id = merge_request_diffs.merge_request_id - )'.squish - - MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation| - relation.update_all(update) - end - end -end diff --git a/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb new file mode 100644 index 00000000000..7a63382cc6d --- /dev/null +++ b/db/post_migrate/20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations.rb @@ -0,0 +1,29 @@ +class ScheduleMergeRequestLatestMergeRequestDiffIdMigrations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 50_000 + MIGRATION = 'PopulateMergeRequestsLatestMergeRequestDiffId' + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + # On GitLab.com, we saw that we generated about 500,000 dead tuples over 5 minutes. + # To keep replication lag from ballooning, we'll aim for 50,000 updates over 5 minutes. + # + # Assuming that there are 5 million rows affected (which is more than on + # GitLab.com), and that each batch of 50,000 rows takes up to 5 minutes, then + # we can migrate all the rows in 8.5 hours. + def up + MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation, index| + range = relation.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker.perform_in(index * 5.minutes, MIGRATION, range) + end + end +end diff --git a/db/post_migrate/20171114104051_remove_empty_fork_networks.rb b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb new file mode 100644 index 00000000000..2fe99a1b9c1 --- /dev/null +++ b/db/post_migrate/20171114104051_remove_empty_fork_networks.rb @@ -0,0 +1,36 @@ +class RemoveEmptyForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 10_000 + + class MigrationForkNetwork < ActiveRecord::Base + include EachBatch + + self.table_name = 'fork_networks' + end + + class MigrationForkNetworkMembers < ActiveRecord::Base + self.table_name = 'fork_network_members' + end + + disable_ddl_transaction! + + def up + say 'Deleting empty ForkNetworks in batches' + + has_members = MigrationForkNetworkMembers + .where('fork_network_members.fork_network_id = fork_networks.id') + .select(1) + MigrationForkNetwork.where('NOT EXISTS (?)', has_members) + .each_batch(of: BATCH_SIZE) do |networks| + deleted = networks.delete_all + + say "Deleted #{deleted} rows in batch" + end + end + + def down + # nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index 37e08d453c8..7afab18df08 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171106180641) do +ActiveRecord::Schema.define(version: 20171114162227) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -140,6 +140,15 @@ ActiveRecord::Schema.define(version: 20171106180641) do t.integer "circuitbreaker_storage_timeout", default: 30 t.integer "circuitbreaker_access_retries", default: 3 t.integer "circuitbreaker_backoff_threshold", default: 80 + t.boolean "throttle_unauthenticated_enabled", default: false, null: false + t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false + t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false + t.boolean "throttle_authenticated_api_enabled", default: false, null: false + t.integer "throttle_authenticated_api_requests_per_period", default: 7200, null: false + t.integer "throttle_authenticated_api_period_in_seconds", default: 3600, null: false + t.boolean "throttle_authenticated_web_enabled", default: false, null: false + t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false + t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false end create_table "audit_events", force: :cascade do |t| @@ -1031,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do create_table "merge_requests", force: :cascade do |t| t.string "target_branch", null: false t.string "source_branch", null: false - t.integer "source_project_id", null: false + t.integer "source_project_id" t.integer "author_id" t.integer "assignee_id" t.string "title" @@ -1071,6 +1080,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree + add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree @@ -1079,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} + add_index "merge_requests", ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree create_table "merge_requests_closing_issues", force: :cascade do |t| t.integer "merge_request_id", null: false @@ -1956,7 +1967,13 @@ ActiveRecord::Schema.define(version: 20171106180641) do add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify + add_foreign_key "merge_requests", "milestones", name: "fk_6a5165a692", on_delete: :nullify + add_foreign_key "merge_requests", "projects", column: "source_project_id", name: "fk_3308fe130c", on_delete: :nullify add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade + add_foreign_key "merge_requests", "users", column: "assignee_id", name: "fk_6149611a04", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "author_id", name: "fk_e719a85f8a", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "merge_user_id", name: "fk_ad525e1f87", on_delete: :nullify + add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 4d3be0ab8f6..a88e67bfeb5 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -53,7 +53,9 @@ or in different cloud availability zones. > **Note:** GitLab recommends against choosing this HA method because of the complexity of managing DRBD and crafting automatic failover. This is - *compatible* with GitLab, but not officially *supported*. + *compatible* with GitLab, but not officially *supported*. If you are + an EE customer, support will help you with GitLab related problems, but if the + root cause is identified as DRBD, we will not troubleshoot further. Components/Servers Required: 2 servers/virtual machines (one active/one passive) diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index be538ea250a..83a714810c1 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -163,6 +163,34 @@ separate Rails process to debug the issue: 1. In a new window, run `top`. It should show this ruby process using 100% CPU. Write down the PID. 1. Follow step 2 from the previous section on using gdb. +### GitLab: API is not accessible + +This often occurs when gitlab-shell attempts to request authorization via the +internal API (e.g., `http://localhost:8080/api/v4/internal/allowed`), and +something in the check fails. There are many reasons why this may happen: + +1. Timeout connecting to a database (e.g., PostgreSQL or Redis) +1. Error in Git hooks or push rules +1. Error accessing the repository (e.g., stale NFS handles) + +To diagnose this problem, try to reproduce the problem and then see if there +is a unicorn worker that is spinning via `top`. Try to use the `gdb` +techniques above. In addition, using `strace` may help isolate issues: + +```shell +strace -tt -T -f -s 1024 -p <PID of unicorn worker> -o /tmp/unicorn.txt +``` + +If you cannot isolate which Unicorn worker is the issue, try to run `strace` +on all the Unicorn workers to see where the `/internal/allowed` endpoint gets +stuck: + +```shell +ps auwx | grep unicorn | awk '{ print " -p " $2}' | xargs strace -tt -T -f -s 1024 -o /tmp/unicorn.txt +``` + +The output in `/tmp/unicorn.txt` may help diagnose the root cause. + # More information * [Debugging Stuck Ruby Processes](https://blog.newrelic.com/2013/04/29/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9/) diff --git a/doc/api/environments.md b/doc/api/environments.md index e8deb3e07e9..6e20781f51a 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -36,7 +36,7 @@ Creates a new environment with the given name and external_url. It returns `201` if the environment was successfully created, `400` for wrong parameters. ``` -POST /projects/:id/environment +POST /projects/:id/environments ``` | Attribute | Type | Required | Description | diff --git a/doc/api/groups.md b/doc/api/groups.md index 6a6e94195a7..c1b5737c247 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -82,6 +82,8 @@ GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_valu ## List a groups's subgroups +> [Introduced][ce-15142] in GitLab 10.3. + Get a list of visible direct subgroups in this group. When accessed without authentication, only public groups are returned. @@ -513,3 +515,5 @@ And to switch pages add: ``` /groups?per_page=100&page=2 ``` + +[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142 diff --git a/doc/api/settings.md b/doc/api/settings.md index 4e24e4bbfc3..b27220f57f4 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -79,7 +79,7 @@ PUT /application/settings | `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts | -| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | +| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | diff --git a/doc/api/users.md b/doc/api/users.md index aa711090af1..478d747a50d 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -297,6 +297,7 @@ Parameters: - `location` (optional) - User's location - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false +- `skip_reconfirmation` (optional) - Skip reconfirmation - true or false (default) - `external` (optional) - Flags the user as external - true or false(default) - `avatar` (optional) - Image file for user's avatar diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md index c83d3f6f248..286f3dee665 100644 --- a/doc/ci/git_submodules.md +++ b/doc/ci/git_submodules.md @@ -8,7 +8,7 @@ with the use of [SSH keys](ssh_keys/README.md). - With GitLab 8.12 onward, your permissions are used to evaluate what a CI job can access. More information about how this system works can be found in the - [Jobs permissions model](../user/permissions.md#jobs-permissions). + [Jobs permissions model](../user/permissions.md#job-permissions). - The HTTP(S) Git protocol [must be enabled][gitpro] in your GitLab instance. ## Configuring the `.gitmodules` file diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 6ad70707594..f40d2c5e347 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -124,7 +124,7 @@ stages: 1. First, all jobs of `build` are executed in parallel. 1. If all jobs of `build` succeed, the `test` jobs are executed in parallel. 1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel. -1. If all jobs of `deploy` succeed, the commit is marked as `success`. +1. If all jobs of `deploy` succeed, the commit is marked as `passed`. 1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed. diff --git a/doc/development/README.md b/doc/development/README.md index 5690ae68e00..6892838be7f 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -22,6 +22,7 @@ comments: false - [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements - [Frontend guidelines](fe_guide/index.md) +- [Emoji guide](fe_guide/emojis.md) ## Backend guides @@ -70,6 +71,7 @@ comments: false - [Iterating tables in batches](iterating_tables_in_batches.md) - [Ordering table columns](ordering_table_columns.md) - [Verifying database capabilities](verifying_database_capabilities.md) +- [Database Debugging and Troubleshooting](database_debugging.md) ## Testing guides diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md new file mode 100644 index 00000000000..50eb8005b44 --- /dev/null +++ b/doc/development/database_debugging.md @@ -0,0 +1,55 @@ +# Database Debugging and Troubleshooting + +This section is to help give some copy-pasta you can use as a reference when you +run into some head-banging database problems. + +An easy first step is to search for your error in Slack or google "GitLab <my error>". + +--- + +Available `RAILS_ENV` + + - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus) + - `development` (this is your main GDK db) + - `test` (used for tests like rspec and spinach) + + +## Nuke everything and start over + +If you just want to delete everything and start over with an empty DB (~1 minute): + + - `bundle exec rake db:reset RAILS_ENV=development` + +If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations: + + - `bundle exec rake dev:setup RAILS_ENV=development` + +If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important data: + + - `bundle exec rake db:reset RAILS_ENV=test` + +## Migration wrangling + + - `bundle exec rake db:migrate RAILS_ENV=development`: Execute any pending migrations that you may have picked up from a MR + - `bundle exec rake db:migrate:status RAILS_ENV=development`: Check if all migrations are `up` or `down` + - `bundle exec rake db:migrate:down VERSION=20170926203418 RAILS_ENV=development`: Tear down a migration + - `bundle exec rake db:migrate:up VERSION=20170926203418 RAILS_ENV=development`: Setup a migration + - `bundle exec rake db:migrate:redo VERSION=20170926203418 RAILS_ENV=development`: Re-run a specific migration + + +## Manually access the database + +Access the database via one of these commands (they all get you to the same place) + +``` +gdk psql -d gitlabhq_development +bundle exec rails dbconsole RAILS_ENV=development +bundle exec rails db RAILS_ENV=development +``` + + - `\q`: Quit/exit + - `\dt`: List all tables + - `\d+ issues`: List columns for `issues` table + - `CREATE TABLE board_labels();`: Create a table called `board_labels` + - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run + - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md new file mode 100644 index 00000000000..962fe3dcec9 --- /dev/null +++ b/doc/development/fe_guide/axios.md @@ -0,0 +1,68 @@ +# Axios +We use [axios][axios] to communicate with the server in Vue applications and most new code. + +In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`. + +## CSRF token +All our request require a CSRF token. +To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` . + +This exported module should be used instead of directly using `axios` to ensure the token is set. + +## Usage +```javascript + import axios from '~/lib/utils/axios_utils'; + + axios.get(url) + .then((response) => { + // `data` is the response that was provided by the server + const data = response.data; + + // `headers` the headers that the server responded with + // All header names are lower cased + const paginationData = response.headers; + }) + .catch(() => { + //handle the error + }); +``` + +## Mock axios response on tests + +To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter] + + +```javascript + import axios from '~/lib/utils/axios_utils'; + import MockAdapter from 'axios-mock-adapter'; + + let mock; + beforeEach(() => { + // This sets the mock adapter on the default instance + mock = new MockAdapter(axios); + // Mock any GET request to /users + // arguments for reply are (status, data, headers) + mock.onGet('/users').reply(200, { + users: [ + { id: 1, name: 'John Smith' } + ] + }); + }); + + afterEach(() => { + mock.reset(); + }); +``` + +### Mock poll requests on tests with axios + +Because polling function requires an header object, we need to always include an object as the third argument: + +```javascript + mock.onGet('/users').reply(200, { foo: 'bar' }, {}); +``` + +[axios]: https://github.com/axios/axios +[axios-instance]: #creating-an-instance +[axios-interceptors]: https://github.com/axios/axios#interceptors +[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md new file mode 100644 index 00000000000..e1660ac5caa --- /dev/null +++ b/doc/development/fe_guide/dropdowns.md @@ -0,0 +1,38 @@ +# Dropdowns + + +## How to style a bootstrap dropdown +1. Use the HTML structure provided by the [docs][bootstrap-dropdowns] +1. Add a specific class to the top level `.dropdown` element + + + ```Haml + .dropdown.my-dropdown + %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } + %span.dropdown-toggle-text + Toggle Dropdown + = icon('chevron-down') + + %ul.dropdown-menu + %li + %a + item! + ``` + + Or use the helpers + ```Haml + .dropdown.my-dropdown + = dropdown_toggle('Toogle!', { toggle: 'dropdown' }) + = dropdown_content + %li + %a + item! + ``` + +1. Include the mixin in CSS + + ```SCSS + @include new-style-dropdown('.my-dropdown '); + ``` + +[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns diff --git a/doc/development/fe_guide/emojis.md b/doc/development/fe_guide/emojis.md new file mode 100644 index 00000000000..38794c47965 --- /dev/null +++ b/doc/development/fe_guide/emojis.md @@ -0,0 +1,27 @@ +# Emojis + +GitLab supports native unicode emojis and fallsback to image-based emojis selectively +when your platform does not support it. + +# How to update Emojis + + 1. Update the `gemojione` gem + 1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json). + In the future, we could grab the file directly from the gem. + We should probably make a PR on the Gemojione project to get access to + all emojis after being parsed or just a raw path to the `json` file itself. + 1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version) + is up to date with the latest version. + 1. Run `bundle exec rake gemojione:aliases` + 1. Run `bundle exec rake gemojione:digests` + 1. Run `bundle exec rake gemojione:sprite` + 1. Ensure new sprite sheets generated for 1x and 2x + - `app/assets/images/emoji.png` + - `app/assets/images/emoji@2x.png` + 1. Ensure you see new individual images copied into `app/assets/images/emoji/` + 1. Ensure you can see the new emojis and their aliases in the GFM Autocomplete + 1. Ensure you can see the new emojis and their aliases in the award emoji menu + 1. You might need to add new emoji unicode support checks and rules for platforms + that do not support a certain emoji and we need to fallback to an image. + See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js` + and `app/assets/javascripts/emoji/support/unicode_support_map.js` diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index a76e978bd26..b288ee95722 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -4,15 +4,17 @@ We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are on ### Usage in HAML/Rails -To use a sprite Icon in HAML or Rails we use a specific helper function : +To use a sprite Icon in HAML or Rails we use a specific helper function : `sprite_icon(icon_name, size: nil, css_class: '')` -**icon_name** Use the icon_name that you can find in the SVG Sprite (Overview is available under `/assets/sprite.symbol.html`). +**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). + **size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class) + **css_class (optional)** If you want to add additional css classes -**Example** +**Example** `= sprite_icon('issues', size: 72, css_class: 'icon-danger')` @@ -20,16 +22,34 @@ To use a sprite Icon in HAML or Rails we use a specific helper function : `<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>` +### Usage in Vue + +We have a special Vue component for our sprite icons in `\vue_shared\components\icon.vue`. + +Sample usage : + +`<icon + name="retry" + :size="32" + css-classes="top" + />` + +**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`). + +**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes) + +**css-classes (optional)** Additional CSS Classes to add to the svg tag. + ### Usage in HTML/JS -Please use the following function inside JS to render an icon : +Please use the following function inside JS to render an icon : `gl.utils.spriteIcon(iconName)` ## Adding a new icon to the sprite All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. -To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. +To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced. # SVG Illustrations diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 8f956681693..72cb557d054 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -71,12 +71,14 @@ Vue specific design patterns and practices. --- -## [Vue Resource](vue_resource.md) -Vue resource specific practices and gotchas. +## [Axios](axios.md) +Axios specific practices and gotchas. ## [Icons](icons.md) How we use SVG for our Icons. +## [Dropdowns](dropdowns.md) +How we use dropdowns. --- ## Style Guides diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index f88f0753687..6e9f18dd1c3 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -178,16 +178,13 @@ itself, please read this guide: [State Management][state-management] The Service is a class used only to communicate with the server. It does not store or manipulate any data. It is not aware of the store or the components. -We use [vue-resource][vue-resource-repo] to communicate with the server. -Refer to [vue resource](vue_resource.md) for more details. +We use [axios][axios] to communicate with the server. +Refer to [axios](axios.md) for more details. -Vue Resource should only be imported in the service file. +Axios instance should only be imported in the service file. ```javascript - import Vue from 'vue'; - import VueResource from 'vue-resource'; - - Vue.use(VueResource); + import axios from 'javascripts/lib/utils/axios_utils'; ``` ### End Result @@ -230,15 +227,14 @@ export default class Store { } // service.js -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import 'vue_shared/vue_resource_interceptor'; - -Vue.use(VueResource); +import axios from 'javascripts/lib/utils/axios_utils' export default class Service { constructor(options) { - this.todos = Vue.resource(endpoint.todosEndpoint); + this.todos = axios.create({ + baseURL: endpoint.todosEndpoint + }); + } getTodos() { @@ -477,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that: ### Stubbing API responses -[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with -the response we need: - - ```javascript - // Mock the service to return data - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify([{ - title: 'This is a todo', - body: 'This is the text' - }]), { - status: 200, - })); - }; +Refer to [mock axios](axios.md#mock-axios-response-on-tests) - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); - - it('should do something', (done) => { - setTimeout(() => { - // Test received data - done(); - }, 0); - }); - ``` - -1. Headers interceptor -Refer to [this section](vue.md#headers) - -1. Use `$.mount()` to mount the component - -```javascript -// bad -new Component({ - el: document.createElement('div') -}); - -// good -new Component().$mount(); -``` ## Vuex To manage the state of an application you may use [Vuex][vuex-docs]. @@ -721,7 +675,6 @@ describe('component', () => { [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow -[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors [vue-test]: https://vuejs.org/v2/guide/unit-testing.html [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [flux]: https://facebook.github.io/flux @@ -729,3 +682,6 @@ describe('component', () => { [vuex-structure]: https://vuex.vuejs.org/en/structure.html [vuex-mutations]: https://vuex.vuejs.org/en/mutations.html [vuex-testing]: https://vuex.vuejs.org/en/testing.html +[axios]: https://github.com/axios/axios +[axios-interceptors]: https://github.com/axios/axios#interceptors + diff --git a/doc/development/fe_guide/vue_resource.md b/doc/development/fe_guide/vue_resource.md deleted file mode 100644 index c376c5c32bf..00000000000 --- a/doc/development/fe_guide/vue_resource.md +++ /dev/null @@ -1,72 +0,0 @@ -# Vue Resouce -In Vue applications we use [vue-resource][vue-resource-repo] to communicate with the server. - -## HTTP Status Codes - -### `.json()` -When making a request to the server, you will most likely need to access the body of the response. -Use `.json()` to convert. Because `.json()` returns a Promise the follwoing structure should be used: - - ```javascript - service.get('url') - .then(resp => resp.json()) - .then((data) => { - this.store.storeData(data); - }) - .catch(() => new Flash('Something went wrong')); - ``` - - -When using `Poll` (`app/assets/javascripts/lib/utils/poll.js`), the `successCallback` needs to handle `.json()` as a Promise: - ```javascript - successCallback: (response) => { - return response.json().then((data) => { - // handle the response - }); - } - ``` - -### 204 -Some endpoints - usually `delete` endpoints - return `204` as the success response. -When handling `204 - No Content` responses, we cannot use `.json()` since it tries to parse the non-existant body content. - -When handling `204` responses, do not use `.json`, otherwise the promise will throw an error and will enter the `catch` statement: - -```javascript - Vue.http.delete('path') - .then(() => { - // success! - }) - .catch(() => { - // handle error - }) -``` - -## Headers -Headers are being parsed into a plain object in an interceptor. -In Vue-resource 1.x `headers` object was changed into an `Headers` object. In order to not change all old code, an interceptor was added. - -If you need to write a unit test that takes the headers in consideration, you need to include an interceptor to parse the headers after your test interceptor. -You can see an example in `spec/javascripts/environments/environment_spec.js`: - ```javascript - import { headersInterceptor } from './helpers/vue_resource_helper'; - - beforeEach(() => { - Vue.http.interceptors.push(myInterceptor); - Vue.http.interceptors.push(headersInterceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, myInterceptor); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); - ``` - -## CSRF token -We use a Vue Resource interceptor to manage the CSRF token. -`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors. -Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js` -since it's already being loaded by `common_vue.js`. - - -[vue-resource-repo]: https://github.com/pagekit/vue-resource diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 9b8ab5da74e..a235dd74909 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -198,7 +198,43 @@ end Keep in mind that this operation can easily take 10-15 minutes to complete on larger installations (e.g. GitLab.com). As a result you should only add default -values if absolutely necessary. +values if absolutely necessary. There is a RuboCop cop that will fail if this +method is used on some tables that are very large on GitLab.com, which would +cause other issues. + +## Updating an existing column + +To update an existing column to a particular value, you can use +`update_column_in_batches` (`add_column_with_default` uses this internally to +fill in the default value). This will split the updates into batches, so we +don't update too many rows at in a single statement. + +This updates the column `foo` in the `projects` table to 10, where `some_column` +is `'hello'`: + +```ruby +update_column_in_batches(:projects, :foo, 10) do |table, query| + query.where(table[:some_column].eq('hello')) +end +``` + +To perform a computed update, the value can be wrapped in `Arel.sql`, so Arel +treats it as an SQL literal. The below example is the same as the one above, but +the value is set to the product of the `bar` and `baz` columns: + +```ruby +update_value = Arel.sql('bar * baz') + +update_column_in_batches(:projects, :foo, update_value) do |table, query| + query.where(table[:some_column].eq('hello')) +end +``` + +Like `add_column_with_default`, there is a RuboCop cop to detect usage of this +on large tables. In the case of `update_column_in_batches`, it may be acceptable +to run on a large table, as long as it is only updating a small subset of the +rows in the table, but do not ignore that without validating on the GitLab.com +staging environment - or asking someone else to do so for you - beforehand. ## Integer column type diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index bfd80aab6a4..4773b6773e8 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -122,6 +122,15 @@ they can be easily inspected. bundle exec rake services:doc ``` +## Updating Emoji Aliases + +To update the Emoji aliases file (used for Emoji autocomplete) you must run the +following: + +``` +bundle exec rake gemojione:aliases +``` + ## Updating Emoji Digests To update the Emoji digests file (used for Emoji autocomplete) you must run the @@ -131,6 +140,7 @@ following: bundle exec rake gemojione:digests ``` + This will update the file `fixtures/emojis/digests.json` based on the currently available Emoji. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 7bf126eec5d..baecf9455b0 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -80,13 +80,13 @@ errors during usage. - 256GB RAM supports up to 32,000 users - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) -We recommend having at least 2GB of swap on your server, even if you currently have +We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have enough available RAM. Having swap will help reduce the chance of errors occurring if your available memory changes. We also recommend [configuring the kernel's swappiness setting](https://askubuntu.com/a/103916) to a low value like `10` to make the most of your RAM while still having the swap available when needed. -Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. +Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those. ## Database @@ -146,7 +146,7 @@ So for a machine with 2 cores, 3 unicorn workers is ideal. For all machines that have 2GB and up we recommend a minimum of three unicorn workers. If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. -To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). +To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). ## Redis and Sidekiq diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 372e1909330..075feaeead9 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -22,6 +22,7 @@ Visit the links below for details: - [Redmine](../user/project/integrations/redmine.md) - [Jira](../user/project/integrations/jira.md) - [Bugzilla](../user/project/integrations/bugzilla.md) +- [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md) ### Service Template diff --git a/doc/user/discussions/img/image_resolved_discussion.png b/doc/user/discussions/img/image_resolved_discussion.png Binary files differindex ed00b5c77fe..ed00b5c77fe 100755..100644 --- a/doc/user/discussions/img/image_resolved_discussion.png +++ b/doc/user/discussions/img/image_resolved_discussion.png diff --git a/doc/user/discussions/img/onion_skin_view.png b/doc/user/discussions/img/onion_skin_view.png Binary files differindex 91c3b396844..91c3b396844 100755..100644 --- a/doc/user/discussions/img/onion_skin_view.png +++ b/doc/user/discussions/img/onion_skin_view.png diff --git a/doc/user/discussions/img/swipe_view.png b/doc/user/discussions/img/swipe_view.png Binary files differindex 82d6e52173c..82d6e52173c 100755..100644 --- a/doc/user/discussions/img/swipe_view.png +++ b/doc/user/discussions/img/swipe_view.png diff --git a/doc/user/discussions/img/two_up_view.png b/doc/user/discussions/img/two_up_view.png Binary files differindex d9e90708e87..d9e90708e87 100755..100644 --- a/doc/user/discussions/img/two_up_view.png +++ b/doc/user/discussions/img/two_up_view.png diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index f2ad42f21fd..022d6317555 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -55,9 +55,10 @@ You have 6 options here that you can use for your default dashboard view: The project home page content setting allows you to choose what content you want to see on a project’s home page. -You can choose between 2 options: +You can choose between 3 options: - Show the files and the readme (default) +- Show the readme - Show the project’s activity [rouge]: http://rouge.jneen.net/ "Rouge website" diff --git a/doc/user/project/clusters/img/cluster-applications.png b/doc/user/project/clusters/img/cluster-applications.png Binary files differdeleted file mode 100644 index 7c82d19297e..00000000000 --- a/doc/user/project/clusters/img/cluster-applications.png +++ /dev/null diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 27b4b49c207..cf0c7c109a8 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -1,14 +1,15 @@ -# Connecting GitLab with GKE +# Connecting GitLab with a Kubernetes cluster > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1. CAUTION: **Warning:** The Cluster integration is currently in **Beta**. -Connect your project to Google Container Engine (GKE) in a few steps. - With a cluster associated to your project, you can use Review Apps, deploy your -applications, run your pipelines, and much more in an easy way. +applications, run your pipelines, and much more, in an easy way. + +Connect your project to Google Kubernetes Engine (GKE) or your own Kubernetes +cluster in a few steps. NOTE: **Note:** The Cluster integration will eventually supersede the @@ -30,36 +31,58 @@ prerequisites must be met: - You must have Master [permissions] in order to be able to access the **Cluster** page. -If all of the above requirements are met, you can proceed to add a new cluster. +If all of the above requirements are met, you can proceed to add a new GKE +cluster. ## Adding a cluster NOTE: **Note:** You need Master [permissions] and above to add a cluster. +There are two options when adding a new cluster; either use Google Kubernetes +Engine (GKE) or provide the credentials to your own Kubernetes cluster. + To add a new cluster: -1. Navigate to your project's **CI/CD > Cluster** page. -1. Connect your Google account if you haven't done already by clicking the - "Sign-in with Google" button. -1. Fill in the requested values: - - **Cluster name** (required) - The name you wish to give the cluster. - - **GCP project ID** (required) - The ID of the project you created in your GCP - console that will host the Kubernetes cluster. This must **not** be confused - with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). - - **Zone** - The zone under which the cluster will be created. Read more about - [the available zones](https://cloud.google.com/compute/docs/regions-zones/). - - **Number of nodes** - The number of nodes you wish the cluster to have. - - **Machine type** - The machine type of the Virtual Machine instance that - the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types). - - **Project namespace** - The unique namespace for this project. By default you - don't have to fill it in; by leaving it blank, GitLab will create one for you. -1. Click the **Create cluster** button. - -After a few moments your cluster should be created. If something goes wrong, +1. Navigate to your project's **CI/CD > Cluster** page +1. If you want to let GitLab create a cluster on GKE for you, go through the + following steps, otherwise skip to the next one. + 1. Click on **Create with GKE** + 1. Connect your Google account if you haven't done already by clicking the + **Sign in with Google** button + 1. Fill in the requested values: + - **Cluster name** (required) - The name you wish to give the cluster. + - **GCP project ID** (required) - The ID of the project you created in your GCP + console that will host the Kubernetes cluster. This must **not** be confused + with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). + - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) + under which the cluster will be created. + - **Number of nodes** - The number of nodes you wish the cluster to have. + - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) + of the Virtual Machine instance that the cluster will be based on. + - **Project namespace** - The unique namespace for this project. By default you + don't have to fill it in; by leaving it blank, GitLab will create one for you. +1. If you want to use your own existing Kubernetes cluster, click on + **Add an existing cluster** and fill in the details as described in the + [Kubernetes integration](../integrations/kubernetes.md) documentation. +1. Finally, click the **Create cluster** button + +After a few moments, your cluster should be created. If something goes wrong, you will be notified. -Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration). +You can now proceed to install some pre-defined applications and then +enable the Cluster integration. + +## Installing applications + +GitLab provides a one-click install for various applications which will be +added directly to your configured cluster. Those applications are needed for +[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). + +| Application | GitLab version | Description | +| ----------- | :------------: | ----------- | +| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | +| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | ## Enabling or disabling the Cluster integration @@ -88,12 +111,3 @@ To remove the Cluster integration from your project, simply click on the and [add a cluster](#adding-a-cluster) again. [permissions]: ../../permissions.md - -## Installing applications - -GitLab provides a one-click install for -[Helm Tiller](https://docs.helm.sh/) and -[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) -which will be added directly to your configured cluster. - -![Cluster application settings](img/cluster-applications.png) diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md new file mode 100644 index 00000000000..757522c2ae3 --- /dev/null +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -0,0 +1,20 @@ +# Custom Issue Tracker Service + +To enable the Custom Issue Tracker integration in a project, navigate to the +[Integrations page](project_services.md#accessing-the-project-services), click +the **Customer Issue Tracker** service, and fill in the required details on the page as described +in the table below. + +| Field | Description | +| ----- | ----------- | +| `title` | A title for the issue tracker (to differentiate between instances, for example) | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | Currently unused. Will be changed in a future release. | +| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. | +| `new_issue_url` | Currently unused. Will be changed in a future release. | + + +## Referencing issues + +Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`). +So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`.
\ No newline at end of file diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 271adee7da1..17dcd152363 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -230,7 +230,7 @@ test: - docker run $CI_REGISTRY/group/other-project:latest ``` -[job permissions]: ../permissions.md#jobs-permissions +[job permissions]: ../permissions.md#job-permissions [comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302 [ext]: ../permissions.md#external-users [gitsub]: ../../ci/git_submodules.md diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 453e10184f0..1e19f422d94 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding. If you set up a GitLab Pages project on GitLab.com, it will automatically be accessible under a -[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com). +[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com). The `namespace` is defined by your username on GitLab.com, or the group name you created this project under. diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 9ad15a12c3c..eac706be3a7 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -44,7 +44,7 @@ GitLab CI so that they can be used in your `.gitlab-ci.yml` file. To configure that a job can be executed only when the pipeline has been scheduled (or the opposite), you can use -[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords. +[only and except](../../../ci/yaml/README.md#only-and-except-simplified) configuration keywords. ``` job:on-schedule: diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 56f58fd755a..daa5463d680 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -115,10 +115,12 @@ pages. Depending on the status of your job, a badge can have the following values: +- pending - running -- success +- passed - failed - skipped +- canceled - unknown You can access a pipeline status badge image using the following link: diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json index e2f47db0de2..415dd5a54e0 100644 --- a/fixtures/emojis/aliases.json +++ b/fixtures/emojis/aliases.json @@ -339,6 +339,7 @@ "baguette_bread":"french_bread", "anguished":"frowning", "white_frowning_face":"frowning2", + "rainbow_flag":"gay_pride_flag", "goal_net":"goal", "hammer_and_pick":"hammer_pick", "raised_hand_with_fingers_splayed":"hand_splayed", @@ -488,6 +489,7 @@ "slightly_smiling_face":"slight_smile", "sneeze":"sneezing_face", "speaking_head_in_silhouette":"speaking_head", + "left_speech_bubble":"speech_left", "sleuth_or_spy":"spy", "sleuth_or_spy_tone1":"spy_tone1", "sleuth_or_spy_tone2":"spy_tone2", @@ -537,4 +539,4 @@ "wrestling_tone4":"wrestlers_tone4", "wrestling_tone5":"wrestlers_tone5", "zipper_mouth_face":"zipper_mouth" -} +}
\ No newline at end of file diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 589cff165f3..3c8f6426f93 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -1478,7 +1478,7 @@ }, "cartwheel_tone4": { "category": "activity", - "moji": "🤸🏾,", + "moji": "🤸🏾", "description": "person doing cartwheel tone 4", "unicodeVersion": "9.0", "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e" @@ -5375,6 +5375,13 @@ "unicodeVersion": "6.0", "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1" }, + "gay_pride_flag": { + "category": "flags", + "moji": "🏳🌈", + "description": "gay_pride_flag", + "unicodeVersion": "6.0", + "digest": "924e668c559db61b7f4724a661223081c2fc60d55169f3fe1ad6156934d1d37f" + }, "gemini": { "category": "symbols", "moji": "♊", @@ -7578,7 +7585,7 @@ "moji": "🤶", "description": "mother christmas", "unicodeVersion": "9.0", - "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076" + "digest": "357d769371305a8584f46d6087a962d647b6af22fab363a44702f38ab7814091" }, "mrs_claus_tone1": { "category": "people", @@ -10709,6 +10716,13 @@ "unicodeVersion": "6.0", "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca" }, + "speech_left": { + "category": "symbols", + "moji": "🗨", + "description": "left speech bubble", + "unicodeVersion": "7.0", + "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0" + }, "speedboat": { "category": "travel", "moji": "🚤", diff --git a/fixtures/emojis/generate_aliases.rb b/fixtures/emojis/generate_aliases.rb deleted file mode 100755 index 8838fb9a3af..00000000000 --- a/fixtures/emojis/generate_aliases.rb +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -require 'json' - -aliases = {} - -index_file = File.expand_path("./index.json") -index = JSON.parse(File.read(index_file)) - -index.each_pair do |key, data| - data['aliases'].each do |a| - a.tr!(':', '') - - aliases[a] = key - end -end - -puts JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '') diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json index 2a990913b9c..f55571d31fa 100644 --- a/fixtures/emojis/index.json +++ b/fixtures/emojis/index.json @@ -4023,7 +4023,7 @@ ], "aliases_ascii": [], "keywords": [], - "moji": "🤸🏾," + "moji": "🤸🏾" }, "cartwheel_tone5": { "unicode": "1F938-1F3FF", @@ -14475,6 +14475,19 @@ ], "moji": "💎" }, + "gay_pride_flag": { + "unicode": "1F3F3-1F308", + "unicode_alternates": [], + "name": "gay_pride_flag", + "shortname": ":gay_pride_flag:", + "category": "extras", + "aliases": [ + ":rainbow_flag:" + ], + "aliases_ascii": [], + "keywords": [], + "moji": "🏳🌈" + }, "gemini": { "unicode": "264A", "unicode_alternates": [ @@ -16830,7 +16843,6 @@ "0:-)", "0:)", "0;^)", - "O:-)", "O:)", "O;-)", "O=)", @@ -28506,6 +28518,21 @@ ], "moji": "💬" }, + "speech_left": { + "unicode": "1F5E8", + "unicode_alternates": [ + "1F5E8-FE0F" + ], + "name": "left speech bubble", + "shortname": ":speech_left:", + "category": "symbols", + "aliases": [ + ":left_speech_bubble:" + ], + "aliases_ascii": [], + "keywords": [], + "moji": "🗨" + }, "speedboat": { "unicode": "1F6A4", "unicode_alternates": [], @@ -33477,4 +33504,4 @@ ], "moji": "💤" } -} +}
\ No newline at end of file diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c1c0d344917..9aeebc34525 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -6,9 +6,6 @@ module API module APIGuard extend ActiveSupport::Concern - PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN".freeze - PRIVATE_TOKEN_PARAM = :private_token - included do |base| # OAuth2 Resource Server Authentication use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| @@ -42,7 +39,7 @@ module API # Helper Methods for Grape Endpoint module HelperMethods - include Gitlab::Utils::StrongMemoize + include Gitlab::Auth::UserAuthFinders def find_current_user! user = find_user_from_access_token || find_user_from_warden @@ -53,76 +50,8 @@ module API user end - def access_token - strong_memoize(:access_token) do - find_oauth_access_token || find_personal_access_token - end - end - - def validate_access_token!(scopes: []) - return unless access_token - - case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) - when AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when AccessTokenValidationService::EXPIRED - raise ExpiredError - when AccessTokenValidationService::REVOKED - raise RevokedError - end - end - private - def find_user_from_access_token - return unless access_token - - validate_access_token! - - access_token.user || raise(UnauthorizedError) - end - - # Check the Rails session for valid authentication details - def find_user_from_warden - warden.try(:authenticate) if verified_request? - end - - def warden - env['warden'] - end - - # Check if the request is GET/HEAD, or if CSRF token is valid. - def verified_request? - Gitlab::RequestForgeryProtection.verified?(env) - end - - def find_oauth_access_token - token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) - return unless token - - # Expiration, revocation and scopes are verified in `find_user_by_access_token` - access_token = OauthAccessToken.by_token(token) - raise UnauthorizedError unless access_token - - access_token.revoke_previous_refresh_token! - access_token - end - - def find_personal_access_token - token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s - return unless token.present? - - # Expiration, revocation and scopes are verified in `find_user_by_access_token` - access_token = PersonalAccessToken.find_by(token: token) - raise UnauthorizedError unless access_token - - access_token - end - - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end - # An array of scopes that were registered (using `allow_access_with_scope`) # for the current endpoint class. It also returns scopes registered on # `API::API`, since these are meant to apply to all API routes. @@ -145,8 +74,11 @@ module API private def install_error_responders(base) - error_classes = [MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] + error_classes = [Gitlab::Auth::MissingTokenError, + Gitlab::Auth::TokenNotFoundError, + Gitlab::Auth::ExpiredError, + Gitlab::Auth::RevokedError, + Gitlab::Auth::InsufficientScopeError] base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend end @@ -155,25 +87,25 @@ module API proc do |e| response = case e - when MissingTokenError + when Gitlab::Auth::MissingTokenError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - when TokenNotFoundError + when Gitlab::Auth::TokenNotFoundError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Bad Access Token.") - when ExpiredError + when Gitlab::Auth::ExpiredError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Token is expired. You can either do re-authorization or token refresh.") - when RevokedError + when Gitlab::Auth::RevokedError Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( :invalid_token, "Token was revoked. You have to re-authorize from the user.") - when InsufficientScopeError + when Gitlab::Auth::InsufficientScopeError # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) # does not include WWW-Authenticate header, which breaks the standard. Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( @@ -186,22 +118,5 @@ module API end end end - - # - # Exceptions - # - - MissingTokenError = Class.new(StandardError) - TokenNotFoundError = Class.new(StandardError) - ExpiredError = Class.new(StandardError) - RevokedError = Class.new(StandardError) - UnauthorizedError = Class.new(StandardError) - - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes.map { |s| s.try(:name) || s } - end - end end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2bc4039b019..38e05074353 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -180,10 +180,12 @@ module API if params[:path] commit.raw_diffs(limits: false).each do |diff| next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3c8960cb1ab..b26c61ab8da 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -398,7 +398,7 @@ module API begin @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! } - rescue APIGuard::UnauthorizedError + rescue Gitlab::Auth::UnauthorizedError unauthorized! end end diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 0a8f3073a50..dd4f6c41131 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -4,6 +4,7 @@ module API class Absence < Grape::Validations::Base def validate_param!(attr_name, params) return if params.respond_to?(:key?) && !params.key?(attr_name) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 4c0db4d42b1..4b3c473b0bb 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -36,6 +36,18 @@ module API {} end + def fix_git_env_repository_paths(env, repository_path) + if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence + env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative) + end + + if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence + env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) } + end + + env + end + def log_user_activity(actor) commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 282af32ca94..2cae53dba53 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -14,6 +14,7 @@ module API def get_runner_version_from_params return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 6e78ac2c903..451121a4cea 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -19,7 +19,9 @@ module API status 200 # Stores some Git-specific env thread-safely - Gitlab::Git::Env.set(parse_env) + env = parse_env + env = fix_git_env_repository_paths(env, repository_path) if project + Gitlab::Git::Env.set(env) actor = if params[:key_id] diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 0b9ab4eeb05..ceaaeca4046 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -33,7 +33,7 @@ module API # paginate() only works with a relation. This could lead to a # mismatch between the pagination headers info and the actual notes # array returned, but this is really a edge-case. - paginate(noteable.notes) + paginate(noteable.notes.with_metadata) .reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note else @@ -50,7 +50,7 @@ module API end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do noteable = find_project_noteable(noteables_str, params[:noteable_id]) - note = noteable.notes.find(params[:note_id]) + note = noteable.notes.with_metadata.find(params[:note_id]) can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) if can_read_note diff --git a/lib/api/runners.rb b/lib/api/runners.rb index d3559ef71be..e816fcdd928 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -165,17 +165,20 @@ module API def authenticate_show_runner!(runner) return if runner.is_shared || current_user.admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) end def authenticate_update_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("No access granted") unless user_can_access_runner?(runner) end def authenticate_delete_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 forbidden!("No access granted") unless user_can_access_runner?(runner) @@ -185,6 +188,7 @@ module API forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner is locked") if runner.locked? return if current_user.admin? + forbidden!("No access granted") unless user_can_access_runner?(runner) end diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 00eb7c60f16..c736cc32021 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -95,6 +95,7 @@ module API put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet attrs = declared_params(include_missing: false).merge(request: request, api: true) diff --git a/lib/api/users.rb b/lib/api/users.rb index d80b364bd09..0cd89b1bcf8 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -31,7 +31,6 @@ module API optional :location, type: String, desc: 'The location of the user' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' - optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' optional :avatar, type: File, desc: 'Avatar image for user' all_or_none_of :extern_uid, :provider @@ -101,6 +100,7 @@ module API requires :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' + optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed' at_least_one_of :password, :reset_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' @@ -134,6 +134,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' optional :email, type: String, desc: 'The email of the user' optional :password, type: String, desc: 'The password of the new user' + optional :skip_reconfirmation, type: Boolean, desc: 'Flag indicating the account skips the confirmation by email' optional :name, type: String, desc: 'The name of the user' optional :username, type: String, desc: 'The username of the user' use :optional_attributes diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index be360fbfc0c..0ef26aa696a 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -169,10 +169,12 @@ module API if params[:path] commit.raw_diffs(limits: false).each do |diff| next unless diff.new_path == params[:path] + lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line) lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb index faa265f3314..c6d9957d452 100644 --- a/lib/api/v3/runners.rb +++ b/lib/api/v3/runners.rb @@ -51,6 +51,7 @@ module API helpers do def authenticate_delete_runner!(runner) return if current_user.admin? + forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 forbidden!("No access granted") unless user_can_access_runner?(runner) diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb index 0762fc02d70..126ec72248e 100644 --- a/lib/api/v3/snippets.rb +++ b/lib/api/v3/snippets.rb @@ -91,6 +91,7 @@ module API put ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :update_personal_snippet, snippet attrs = declared_params(include_missing: false) @@ -113,6 +114,7 @@ module API delete ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet snippet.destroy no_content! diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 3ad09a1b421..b6d273b98c2 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -7,12 +7,16 @@ module Backup prepare Project.find_each(batch_size: 1000) do |project| - progress.print " * #{project.full_path} ... " + progress.print " * #{display_repo_path(project)} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) - # Create namespace dir if missing - FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace + # Create namespace dir or hashed path if missing + if project.hashed_storage?(:repository) + FileUtils.mkdir_p(File.dirname(File.join(backup_repos_path, project.disk_path))) + else + FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace + end if empty_repo?(project) progress.puts "[SKIPPED]".color(:cyan) @@ -42,7 +46,7 @@ module Backup path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_repo) - progress.print " * #{wiki.full_path} ... " + progress.print " * #{display_repo_path(wiki)} ... " if empty_repo?(wiki) progress.puts " [SKIPPED]".color(:cyan) else @@ -71,7 +75,7 @@ module Backup end Project.find_each(batch_size: 1000) do |project| - progress.print " * #{project.full_path} ... " + progress.print " * #{display_repo_path(project)} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) @@ -104,7 +108,7 @@ module Backup path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_bundle) - progress.print " * #{wiki.full_path} ... " + progress.print " * #{display_repo_path(wiki)} ... " # If a wiki bundle exists, first remove the empty repo # that was initialized with ProjectWiki.new() and then @@ -185,14 +189,14 @@ module Backup def progress_warn(project, cmd, output) progress.puts "[WARNING] Executing #{cmd}".color(:orange) - progress.puts "Ignoring error on #{project.full_path} - #{output}".color(:orange) + progress.puts "Ignoring error on #{display_repo_path(project)} - #{output}".color(:orange) end def empty_repo?(project_or_wiki) project_or_wiki.repository.expire_exists_cache # protect backups from stale cache project_or_wiki.repository.empty_repo? rescue => e - progress.puts "Ignoring repository error and continuing backing up project: #{project_or_wiki.full_path} - #{e.message}".color(:orange) + progress.puts "Ignoring repository error and continuing backing up project: #{display_repo_path(project_or_wiki)} - #{e.message}".color(:orange) false end @@ -204,5 +208,9 @@ module Backup def progress $progress end + + def display_repo_path(project) + project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path + end end end diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index 9bb8ed913d8..ecb3affbba5 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -86,6 +86,7 @@ module Banzai def save_options return {} unless base_context[:xhtml] + { save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML } end end diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb index fb2faae02bc..a19a05e8c0d 100644 --- a/lib/banzai/querying.rb +++ b/lib/banzai/querying.rb @@ -52,8 +52,10 @@ module Banzai children.each do |child| next if child.text.blank? + node = nodes.shift break unless node == child + filtered_nodes << node end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 4d336068861..8932d4f2905 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -31,6 +31,7 @@ module Banzai nodes.each do |node| if node.has_attribute?(group_attr) next unless can_read_group_reference?(node, user, groups) + visible << node elsif can_read_project_reference?(node) visible << node diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index 5cb9adf52b0..0050295eeda 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -149,6 +149,7 @@ module Banzai def self.full_cache_key(cache_key, pipeline_name) return unless cache_key + ["banzai", *cache_key, pipeline_name || :full] end @@ -157,6 +158,7 @@ module Banzai # method. def self.full_cache_multi_key(cache_key, pipeline_name) return unless cache_key + Rails.cache.__send__(:expanded_key, full_cache_key(cache_key, pipeline_name)) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb index ae65653645b..b1949d693ad 100644 --- a/lib/declarative_policy.rb +++ b/lib/declarative_policy.rb @@ -30,6 +30,7 @@ module DeclarativePolicy policy_class = class_for_class(subject.class) raise "no policy for #{subject.class.name}" if policy_class.nil? + policy_class end @@ -84,6 +85,7 @@ module DeclarativePolicy while subject.respond_to?(:declarative_policy_delegate) raise ArgumentError, "circular delegations" if seen.include?(subject.object_id) + seen << subject.object_id subject = subject.declarative_policy_delegate end diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb index b028169f500..47542194497 100644 --- a/lib/declarative_policy/base.rb +++ b/lib/declarative_policy/base.rb @@ -276,6 +276,7 @@ module DeclarativePolicy # boolean `false` def cache(key, &b) return @cache[key] if cached?(key) + @cache[key] = yield end @@ -291,6 +292,7 @@ module DeclarativePolicy @_conditions[name] ||= begin raise "invalid condition #{name}" unless self.class.conditions.key?(name) + ManifestCondition.new(self.class.conditions[name], self) end end diff --git a/lib/declarative_policy/cache.rb b/lib/declarative_policy/cache.rb index 0804edba016..780d8f707bd 100644 --- a/lib/declarative_policy/cache.rb +++ b/lib/declarative_policy/cache.rb @@ -3,6 +3,7 @@ module DeclarativePolicy class << self def user_key(user) return '<anonymous>' if user.nil? + id_for(user) end @@ -15,6 +16,7 @@ module DeclarativePolicy def subject_key(subject) return '<nil>' if subject.nil? return subject.inspect if subject.is_a?(Symbol) + "#{subject.class.name}:#{id_for(subject)}" end diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb index 7cfa82a9a9f..e309244a3b3 100644 --- a/lib/declarative_policy/rule.rb +++ b/lib/declarative_policy/rule.rb @@ -83,6 +83,7 @@ module DeclarativePolicy def cached_pass?(context) condition = context.condition(@name) return nil unless condition.cached? + condition.pass? end @@ -109,6 +110,7 @@ module DeclarativePolicy def delegated_context(context) policy = context.delegated_policies[@delegate_name] raise MissingDelegate if policy.nil? + policy end @@ -121,6 +123,7 @@ module DeclarativePolicy def cached_pass?(context) condition = delegated_context(context).condition(@name) return nil unless condition.cached? + condition.pass? rescue MissingDelegate false @@ -157,6 +160,7 @@ module DeclarativePolicy def cached_pass?(context) runner = context.runner(@ability) return nil unless runner.cached? + runner.pass? end @@ -258,6 +262,7 @@ module DeclarativePolicy def score(context) return 0 unless cached_pass?(context).nil? + @rules.map { |r| r.score(context) }.inject(0, :+) end diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb index 45ff2ef9ced..77c91817382 100644 --- a/lib/declarative_policy/runner.rb +++ b/lib/declarative_policy/runner.rb @@ -43,6 +43,7 @@ module DeclarativePolicy # used by Rule::Ability. See #steps_by_score def score return 0 if cached? + steps.map(&:score).inject(0, :+) end diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb index de391de9059..69d981e8be9 100644 --- a/lib/file_size_validator.rb +++ b/lib/file_size_validator.rb @@ -8,6 +8,7 @@ class FileSizeValidator < ActiveModel::EachValidator def initialize(options) if range = (options.delete(:in) || options.delete(:within)) raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range) + options[:minimum], options[:maximum] = range.begin, range.end options[:maximum] -= 1 if range.exclude_end? end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index b4012ebbb99..7127948cf00 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -58,9 +58,9 @@ module Gitlab def protection_options { "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, - "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE, - "Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH, - "Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL + "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE, + "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL } end diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb new file mode 100644 index 00000000000..46ec040ce92 --- /dev/null +++ b/lib/gitlab/auth/request_authenticator.rb @@ -0,0 +1,25 @@ +# Use for authentication only, in particular for Rack::Attack. +# Does not perform authorization of scopes, etc. +module Gitlab + module Auth + class RequestAuthenticator + include UserAuthFinders + + attr_reader :request + + def initialize(request) + @request = request + end + + def user + find_sessionless_user || find_user_from_warden + end + + def find_sessionless_user + find_user_from_access_token || find_user_from_rss_token + rescue Gitlab::Auth::AuthenticationError + nil + end + end + end +end diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb new file mode 100644 index 00000000000..b4114a3ac96 --- /dev/null +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -0,0 +1,109 @@ +module Gitlab + module Auth + # + # Exceptions + # + + AuthenticationError = Class.new(StandardError) + MissingTokenError = Class.new(AuthenticationError) + TokenNotFoundError = Class.new(AuthenticationError) + ExpiredError = Class.new(AuthenticationError) + RevokedError = Class.new(AuthenticationError) + UnauthorizedError = Class.new(AuthenticationError) + + class InsufficientScopeError < AuthenticationError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes.map { |s| s.try(:name) || s } + end + end + + module UserAuthFinders + include Gitlab::Utils::StrongMemoize + + PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze + PRIVATE_TOKEN_PARAM = :private_token + + # Check the Rails session for valid authentication details + def find_user_from_warden + current_request.env['warden']&.authenticate if verified_request? + end + + def find_user_from_rss_token + return unless current_request.path.ends_with?('.atom') || current_request.format.atom? + + token = current_request.params[:rss_token].presence + return unless token + + User.find_by_rss_token(token) || raise(UnauthorizedError) + end + + def find_user_from_access_token + return unless access_token + + validate_access_token! + + access_token.user || raise(UnauthorizedError) + end + + def validate_access_token!(scopes: []) + return unless access_token + + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when AccessTokenValidationService::EXPIRED + raise ExpiredError + when AccessTokenValidationService::REVOKED + raise RevokedError + end + end + + private + + def access_token + strong_memoize(:access_token) do + find_oauth_access_token || find_personal_access_token + end + end + + def find_personal_access_token + token = + current_request.params[PRIVATE_TOKEN_PARAM].presence || + current_request.env[PRIVATE_TOKEN_HEADER].presence + + return unless token + + # Expiration, revocation and scopes are verified in `validate_access_token!` + PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError) + end + + def find_oauth_access_token + token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods) + return unless token + + # Expiration, revocation and scopes are verified in `validate_access_token!` + oauth_token = OauthAccessToken.by_token(token) + raise UnauthorizedError unless oauth_token + + oauth_token.revoke_previous_refresh_token! + oauth_token + end + + # Check if the request is GET/HEAD, or if CSRF token is valid. + def verified_request? + Gitlab::RequestForgeryProtection.verified?(current_request.env) + end + + def ensure_action_dispatch_request(request) + return request if request.is_a?(ActionDispatch::Request) + + ActionDispatch::Request.new(request.env) + end + + def current_request + @current_request ||= ensure_action_dispatch_request(request) + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb new file mode 100644 index 00000000000..7e109e96e73 --- /dev/null +++ b/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id.rb @@ -0,0 +1,30 @@ +module Gitlab + module BackgroundMigration + class PopulateMergeRequestsLatestMergeRequestDiffId + BATCH_SIZE = 1_000 + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + def perform(start_id, stop_id) + update = ' + latest_merge_request_diff_id = ( + SELECT MAX(id) + FROM merge_request_diffs + WHERE merge_requests.id = merge_request_diffs.merge_request_id + )'.squish + + MergeRequest + .where(id: start_id..stop_id) + .where(latest_merge_request_diff_id: nil) + .each_batch(of: BATCH_SIZE) do |relation| + + relation.update_all(update) + end + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 033ecd15749..d48ae17aeaf 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -61,9 +61,9 @@ module Gitlab def import_wiki return if project.wiki.repository_exists? - path_with_namespace = "#{project.full_path}.wiki" + disk_path = project.wiki.disk_path import_url = project.import_url.sub(/\.git\z/, ".git/wiki") - gitlab_shell.import_repository(project.repository_storage_path, path_with_namespace, import_url) + gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url) rescue StandardError => e errors << { type: :wiki, errors: e.message } end diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb index 5b32fca00a4..9c9e6668e6f 100644 --- a/lib/gitlab/changes_list.rb +++ b/lib/gitlab/changes_list.rb @@ -16,6 +16,7 @@ module Gitlab @changes ||= begin @raw_changes.map do |change| next if change.blank? + oldrev, newrev, ref = change.strip.split(' ') { oldrev: oldrev, newrev: newrev, ref: ref } end.compact diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index a788fb3fcbc..0bbd60d8ffe 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -98,6 +98,7 @@ module Gitlab def read_string(gz) string_size = read_uint32(gz) return nil unless string_size + gz.read(string_size) end diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb index 22941d48edf..5b2f09e03ea 100644 --- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -43,6 +43,7 @@ module Gitlab def parent return nil unless has_parent? + self.class.new(@path.to_s.chomp(basename), @entries) end @@ -64,6 +65,7 @@ module Gitlab def directories(opts = {}) return [] unless directory? + dirs = children.select(&:directory?) return dirs unless has_parent? && opts[:parent] @@ -74,6 +76,7 @@ module Gitlab def files return [] unless directory? + children.select(&:file?) end diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index b88b2e36d53..c811f88f483 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -8,6 +8,7 @@ module Gitlab def from_image(job) image = Gitlab::Ci::Build::Image.new(job.options[:image]) return unless image.valid? + image end diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 6555c589173..2844be80a84 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -37,6 +37,7 @@ module Gitlab def value return { name: @config } if string? return @config if hash? + {} end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 0159179f0a9..eb606b57667 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -111,6 +111,7 @@ module Gitlab def validate_string_or_regexp(value) return false unless value.is_a?(String) return validate_regexp(value) if look_like_regexp?(value) + true end end diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index f07fd1dfdda..633de9f9776 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -2,6 +2,7 @@ module Gitlab class Daemon def self.initialize_instance(*args) raise "#{name} singleton instance already initialized" if @instance + @instance = new(*args) Kernel.at_exit(&@instance.method(:stop)) @instance diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 2c35da8f1aa..c276c3566b4 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -220,6 +220,15 @@ module Gitlab # column - The name of the column to update. # value - The value for the column. # + # The `value` argument is typically a literal. To perform a computed + # update, an Arel literal can be used instead: + # + # update_value = Arel.sql('bar * baz') + # + # update_column_in_batches(:projects, :foo, update_value) do |table, query| + # query.where(table[:some_column].eq('hello')) + # end + # # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop # determines this method to be too complex while there's no way to make it # less "complex" without introducing extra methods (which actually will diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb index 5481024db8e..7e492938eac 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -68,6 +68,11 @@ module Gitlab has_one :route, as: :source self.table_name = 'projects' + HASHED_STORAGE_FEATURES = { + repository: 1, + attachments: 2 + }.freeze + def repository_storage_path Gitlab.config.repositories.storages[repository_storage]['path'] end @@ -76,6 +81,13 @@ module Gitlab def self.name 'Project' end + + def hashed_storage?(feature) + raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature) + return false unless respond_to?(:storage_version) + + self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature] + end end end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index 75a75f61953..d32616862f0 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -22,9 +22,11 @@ module Gitlab end def move_project_folders(project, old_full_path, new_full_path) - move_repository(project, old_full_path, new_full_path) - move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") - move_uploads(old_full_path, new_full_path) + unless project.hashed_storage?(:repository) + move_repository(project, old_full_path, new_full_path) + move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") + end + move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments) move_pages(old_full_path, new_full_path) end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index ea5891a028a..d0cfe2386ca 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -25,6 +25,10 @@ module Gitlab @repository = repository @diff_refs = diff_refs @fallback_diff_refs = fallback_diff_refs + + # Ensure items are collected in the the batch + new_blob + old_blob end def position(position_marker, position_type: :text) @@ -95,21 +99,15 @@ module Gitlab end def new_blob - return @new_blob if defined?(@new_blob) - - sha = new_content_sha - return @new_blob = nil unless sha + return unless new_content_sha - @new_blob = repository.blob_at(sha, file_path) + Blob.lazy(repository.project, new_content_sha, file_path) end def old_blob - return @old_blob if defined?(@old_blob) - - sha = old_content_sha - return @old_blob = nil unless sha + return unless old_content_sha - @old_blob = repository.blob_at(sha, old_path) + Blob.lazy(repository.project, old_content_sha, old_path) end def content_sha diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 88ae65cb468..a6007ebf531 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -22,10 +22,7 @@ module Gitlab end def diff_files - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445 - Gitlab::GitalyClient.allow_n_plus_1_calls do - @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } - end + @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } end def diff_file_with_old_path(old_path) diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 55708d42161..2d7b57120a6 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -102,6 +102,7 @@ module Gitlab new_char = b[pos] break if old_char != new_char + length += 1 end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 7dc9cc7c281..8302f30a0a2 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -30,6 +30,7 @@ module Gitlab line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 next if line_old <= 1 && line_new <= 1 # top of file + yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index ccfb908bcca..690b27cde81 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -125,6 +125,7 @@ module Gitlab def find_diff_file(repository) return unless diff_refs.complete? return unless comparison = diff_refs.compare_in(repository.project) + comparison.diffs(paths: paths, expanded: true).diff_files.first end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 0ea534a5fd0..efc2e46d289 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -193,7 +193,7 @@ module Gitlab # Repository is initially cloned with a depth of 20 so we need to fetch # deeper in the case the branch has more than 20 commits on top of master fetch(branch: branch, depth: depth) - fetch(branch: 'master', depth: depth) + fetch(branch: 'master', depth: depth, remote: DEFAULT_CE_PROJECT_URL) merge_base_found? end @@ -201,10 +201,10 @@ module Gitlab raise "\n#{branch} is too far behind master, please rebase it!\n" unless success end - def fetch(branch:, depth:) + def fetch(branch:, depth:, remote: 'origin') step( "Fetching deeper...", - %W[git fetch --depth=#{depth} --prune origin +refs/heads/#{branch}:refs/remotes/origin/#{branch}] + %W[git fetch --depth=#{depth} --prune #{remote} +refs/heads/#{branch}:refs/remotes/origin/#{branch}] ) do |output, status| raise "Fetch failed: #{output}" unless status.zero? end diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb index 5894384da5d..ea80e21532e 100644 --- a/lib/gitlab/email/handler/unsubscribe_handler.rb +++ b/lib/gitlab/email/handler/unsubscribe_handler.rb @@ -16,6 +16,7 @@ module Gitlab noteable = sent_notification.noteable raise NoteableNotFoundError unless noteable + noteable.unsubscribe(sent_notification.recipient) end diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb index 2152182b37f..acb000e3e23 100644 --- a/lib/gitlab/fogbugz_import/client.rb +++ b/lib/gitlab/fogbugz_import/client.rb @@ -45,6 +45,7 @@ module Gitlab project_name = repo(project_id).name res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events') return [] unless res['cases']['count'].to_i > 0 + res['cases']['case'] end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 3dcee681c72..5e426b13ade 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -18,6 +18,7 @@ module Gitlab def execute return true unless repo.valid? + client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri]) @cases = client.cases(@repo.id.to_i) @@ -206,6 +207,7 @@ module Gitlab def format_content(raw_content) return raw_content if raw_content.nil? + linkify_issues(escape_for_markdown(raw_content)) end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index cc6c7609ec7..ddd52136bc4 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -102,6 +102,7 @@ module Gitlab if path_arr.size > 1 return nil unless entry[:type] == :tree + path_arr.shift find_entry_by_path(repository, entry[:oid], path_arr.join('/')) else @@ -178,6 +179,8 @@ module Gitlab ) end end + rescue Rugged::ReferenceError + nil end def rugged_raw(repository, sha, limit:) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index cfb88a0c12b..3cb9b254e6e 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -304,7 +304,13 @@ module Gitlab end def delete_all_refs_except(prefixes) - delete_refs(*all_ref_names_except(prefixes)) + gitaly_migrate(:ref_delete_refs) do |is_enabled| + if is_enabled + gitaly_ref_client.delete_refs(except_with_prefixes: prefixes) + else + delete_refs(*all_ref_names_except(prefixes)) + end + end end # Returns an Array of all ref names, except when it's matching pattern @@ -984,6 +990,10 @@ module Gitlab @attributes.attributes(path) end + def gitattribute(path, name) + attributes(path)[name] + end + def languages(ref = nil) Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled| if is_enabled @@ -1151,6 +1161,11 @@ module Gitlab Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha) end + # Items should be of format [[commit_id, path], [commit_id1, path1]] + def batch_blobs(items, blob_size_limit: nil) + Gitlab::Git::Blob.batch(self, items, blob_size_limit: blob_size_limit) + end + def commit_index(user, branch_name, index, options) committer = user_to_committer(user) @@ -1376,6 +1391,7 @@ module Gitlab end return nil unless tmp_entry.type == :tree + tmp_entry = tmp_entry[dir] end end @@ -1496,6 +1512,7 @@ module Gitlab # Ref names must start with `refs/`. def rugged_ref_exists?(ref_name) raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/') + rugged.references.exist?(ref_name) rescue Rugged::ReferenceError false @@ -1562,6 +1579,7 @@ module Gitlab Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) rescue Rugged::ReferenceError => e raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/ + raise InvalidRef.new("Invalid reference #{start_point}") end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 637e7a0659c..4500482d68f 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -78,7 +78,7 @@ module Gitlab def list_remote_tags(remote) tag_list, exit_code, error = nil - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{full_path} ls-remote --tags #{remote}) + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote}) Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| tag_list = stdout.read @@ -88,7 +88,7 @@ module Gitlab raise RemoteError, error unless exit_code.zero? - tag_list.split('\n') + tag_list.split("\n") end end end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 022d1f249a9..d4a53d32c28 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -58,12 +58,12 @@ module Gitlab end end - def pages - @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled| + def pages(limit: nil) + @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| if is_enabled gitaly_get_all_pages else - gollum_get_all_pages + gollum_get_all_pages(limit: limit) end end end @@ -88,14 +88,23 @@ module Gitlab end end - def page_versions(page_path) + # options: + # :page - The Integer page number. + # :per_page - The number of items per page. + # :limit - Total number of items to return. + def page_versions(page_path, options = {}) current_page = gollum_page_by_path(page_path) - current_page.versions.map do |gollum_git_commit| - gollum_page = gollum_wiki.page(current_page.title, gollum_git_commit.id) - new_version(gollum_page, gollum_git_commit.id) + + commits_from_page(current_page, options).map do |gitlab_git_commit| + gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id) + Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format) end end + def count_page_versions(page_path) + @repository.count_commits(ref: 'HEAD', path: page_path) + end + def preview_slug(title, format) # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid # using Rugged through a Gollum::Wiki instance @@ -110,6 +119,22 @@ module Gitlab private + # options: + # :page - The Integer page number. + # :per_page - The number of items per page. + # :limit - Total number of items to return. + def commits_from_page(gollum_page, options = {}) + unless options[:limit] + options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page + options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i + end + + @repository.log(ref: gollum_page.last_version.id, + path: gollum_page.path, + limit: options[:limit], + offset: options[:offset]) + end + def gollum_wiki @gollum_wiki ||= Gollum::Wiki.new(@repository.path) end @@ -126,8 +151,17 @@ module Gitlab end def new_version(gollum_page, commit_id) - commit = Gitlab::Git::Commit.find(@repository, commit_id) - Gitlab::Git::WikiPageVersion.new(commit, gollum_page&.format) + Gitlab::Git::WikiPageVersion.new(version(commit_id), gollum_page&.format) + end + + def version(commit_id) + commit_find_proc = -> { Gitlab::Git::Commit.find(@repository, commit_id) } + + if RequestStore.active? + RequestStore.fetch([:wiki_version_commit, commit_id]) { commit_find_proc.call } + else + commit_find_proc.call + end end def assert_type!(object, klass) @@ -185,8 +219,8 @@ module Gitlab Gitlab::Git::WikiFile.new(gollum_file) end - def gollum_get_all_pages - gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) } + def gollum_get_all_pages(limit: nil) + gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) } end def gitaly_write_page(name, format, content, commit_details) diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb new file mode 100644 index 00000000000..198a1de91c7 --- /dev/null +++ b/lib/gitlab/gitaly_client/attributes_bag.rb @@ -0,0 +1,31 @@ +module Gitlab + module GitalyClient + # This module expects an `ATTRS` const to be defined on the subclass + # See GitalyClient::WikiFile for an example + module AttributesBag + extend ActiveSupport::Concern + + included do + attr_accessor(*const_get(:ATTRS)) + end + + def initialize(params) + params = params.with_indifferent_access + + attributes.each do |attr| + instance_variable_set("@#{attr}", params[attr]) + end + end + + def ==(other) + attributes.all? do |field| + instance_variable_get("@#{field}") == other.instance_variable_get("@#{field}") + end + end + + def attributes + self.class.const_get(:ATTRS) + end + end + end +end diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb index 54df6304865..d98a0ce988f 100644 --- a/lib/gitlab/gitaly_client/diff.rb +++ b/lib/gitlab/gitaly_client/diff.rb @@ -1,21 +1,9 @@ module Gitlab module GitalyClient class Diff - FIELDS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze + ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze - attr_accessor(*FIELDS) - - def initialize(params) - params.each do |key, val| - public_send(:"#{key}=", val) # rubocop:disable GitlabSecurity/PublicSend - end - end - - def ==(other) - FIELDS.all? do |field| - public_send(field) == other.public_send(field) # rubocop:disable GitlabSecurity/PublicSend - end - end + include AttributesBag end end end diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb index 65d81dc5d46..da243ee2d1a 100644 --- a/lib/gitlab/gitaly_client/diff_stitcher.rb +++ b/lib/gitlab/gitaly_client/diff_stitcher.rb @@ -12,7 +12,7 @@ module Gitlab @rpc_response.each do |diff_msg| if current_diff.nil? - diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS) + diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::ATTRS) # gRPC uses frozen strings by default, and we need to have an unfrozen string as it # gets processed further down the line. So we unfreeze the first chunk of the patch # in case it's the only chunk we receive for this diff. diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index b0c73395cb1..31b04bc2650 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -126,6 +126,15 @@ module Gitlab GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request) end + def delete_refs(except_with_prefixes:) + request = Gitaly::DeleteRefsRequest.new( + repository: @gitaly_repo, + except_with_prefix: except_with_prefixes + ) + + GitalyClient.call(@repository.storage, :ref_service, :delete_refs, request) + end + private def consume_refs_response(response) @@ -137,6 +146,7 @@ module Gitlab enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value + enum_value end diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb index a2e415864e6..47c60c92484 100644 --- a/lib/gitlab/gitaly_client/wiki_file.rb +++ b/lib/gitlab/gitaly_client/wiki_file.rb @@ -1,17 +1,9 @@ module Gitlab module GitalyClient class WikiFile - FIELDS = %i(name mime_type path raw_data).freeze + ATTRS = %i(name mime_type path raw_data).freeze - attr_accessor(*FIELDS) - - def initialize(params) - params = params.with_indifferent_access - - FIELDS.each do |field| - instance_variable_set("@#{field}", params[field]) - end - end + include AttributesBag end end end diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb index 98d96fe6211..7339468e911 100644 --- a/lib/gitlab/gitaly_client/wiki_page.rb +++ b/lib/gitlab/gitaly_client/wiki_page.rb @@ -1,16 +1,12 @@ module Gitlab module GitalyClient class WikiPage - FIELDS = %i(title format url_path path name historical raw_data).freeze + ATTRS = %i(title format url_path path name historical raw_data).freeze - attr_accessor(*FIELDS) + include AttributesBag def initialize(params) - params = params.with_indifferent_access - - FIELDS.each do |field| - instance_variable_set("@#{field}", params[field]) - end + super # All gRPC strings in a response are frozen, so we get an unfrozen # version here so appending to `raw_data` doesn't blow up. diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 8f05f40365e..c8f065f5881 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -94,6 +94,7 @@ module Gitlab page, version = wiki_page_from_iterator(response) { |message| message.end_of_page } break unless page && version + pages << [page, version] end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index f1007daab5d..075b3982608 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -65,6 +65,7 @@ module Gitlab y << item end break if items.empty? || items.size < per_page + page += 1 end end diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index 196f2b6b34c..e29dd0d5b0e 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -28,6 +28,7 @@ module Gitlab SAFE_HOOK_RELATIONS = %i[ assignees labels + total_time_spent ].freeze attr_accessor :issue diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index 503452c8ff3..ae9b68eb648 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -33,6 +33,7 @@ module Gitlab SAFE_HOOK_RELATIONS = %i[ assignee labels + total_time_spent ].freeze attr_accessor :merge_request diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb index 61db4bd9ccc..f3d7407383c 100644 --- a/lib/gitlab/import_export/merge_request_parser.rb +++ b/lib/gitlab/import_export/merge_request_parser.rb @@ -1,7 +1,7 @@ module Gitlab module ImportExport class MergeRequestParser - FORKED_PROJECT_ID = -1 + FORKED_PROJECT_ID = nil def initialize(project, diff_head_sha, merge_request, relation_hash) @project = project diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb index c8479fbc0e8..fbbddb7bffa 100644 --- a/lib/gitlab/kubernetes/namespace.rb +++ b/lib/gitlab/kubernetes/namespace.rb @@ -12,6 +12,7 @@ module Gitlab @client.get_namespace(name) rescue ::KubeException => ke raise ke unless ke.error_code == 404 + false end diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index ed1de73f8c6..7274d1c3b43 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -62,6 +62,7 @@ module Gitlab def user return nil unless ldap_user + Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider) end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 4d5c67ed892..3945df27eed 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -9,11 +9,8 @@ module Gitlab class User < Gitlab::OAuth::User class << self def find_by_uid_and_provider(uid, provider) - uid = Gitlab::LDAP::Person.normalize_dn(uid) + identity = ::Identity.with_extern_uid(provider, uid).take - identity = ::Identity - .where(provider: provider) - .where(extern_uid: uid).last identity && identity.user end end diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb index 12c968805f5..4d096e5a741 100644 --- a/lib/gitlab/legacy_github_import/importer.rb +++ b/lib/gitlab/legacy_github_import/importer.rb @@ -15,6 +15,7 @@ module Gitlab def client return @client if defined?(@client) + unless credentials raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index 8b5a60e6b8b..436a9e9550d 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -96,6 +96,7 @@ module Gitlab def worker_label return {} unless defined?(Unicorn::Worker) + worker_no = ::Prometheus::Client::Support::Unicorn.worker_id if worker_no diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 064299f40c8..ead1acb8d44 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -7,6 +7,7 @@ module Gitlab def sql(event) return unless current_transaction + metric_sql_duration_seconds.observe(current_transaction.labels, event.duration / 1000.0) current_transaction.increment(:sql_duration, event.duration, false) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index cfc6b2a2029..c6a56277922 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -42,12 +42,11 @@ module Gitlab project_url = URI.join(config.gitlab.url, path) import_prefix = strip_url(project_url.to_s) - repository_url = case current_application_settings.enabled_git_access_protocol - when 'ssh' + repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh' shell = config.gitlab_shell port = ":#{shell.ssh_port}" unless shell.ssh_port == 22 "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git" - when 'http', nil + else "#{project_url}.git" end @@ -66,6 +65,7 @@ module Gitlab project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX) return unless project_path_match + path = project_path_match[1] # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`. diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb index 5e4932e4e57..c26656704d7 100644 --- a/lib/gitlab/middleware/read_only.rb +++ b/lib/gitlab/middleware/read_only.rb @@ -58,7 +58,7 @@ module Gitlab end def last_visited_url - @env['HTTP_REFERER'] || rack_session['user_return_to'] || Rails.application.routes.url_helpers.root_url + @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url end def route_hash @@ -74,10 +74,16 @@ module Gitlab end def grack_route + # Calling route_hash may be expensive. Only do it if we think there's a possible match + return false unless request.path.end_with?('.git/git-upload-pack') + route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack' end def lfs_route + # Calling route_hash may be expensive. Only do it if we think there's a possible match + return false unless request.path.end_with?('/info/lfs/objects/batch') + route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch' end end diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb index eb3c9002710..c22d0a84860 100644 --- a/lib/gitlab/multi_collection_paginator.rb +++ b/lib/gitlab/multi_collection_paginator.rb @@ -55,7 +55,9 @@ module Gitlab def first_collection_last_page_size return @first_collection_last_page_size if defined?(@first_collection_last_page_size) - @first_collection_last_page_size = paginated_first_collection(first_collection_page_count).count + @first_collection_last_page_size = paginated_first_collection(first_collection_page_count) + .except(:select) + .size end end end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index b4b3b00c84d..552133234a3 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -157,7 +157,7 @@ module Gitlab end def find_by_uid_and_provider - identity = Identity.find_by(provider: auth_hash.provider, extern_uid: auth_hash.uid) + identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take identity && identity.user end diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb index 962ff4d3985..1d9a5d1a20a 100644 --- a/lib/gitlab/optimistic_locking.rb +++ b/lib/gitlab/optimistic_locking.rb @@ -11,6 +11,7 @@ module Gitlab rescue ActiveRecord::StaleObjectError retries -= 1 raise unless retries >= 0 + subject.reload end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index bd677ec4bf3..2c7b8af83f2 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -25,7 +25,7 @@ module Gitlab # See https://github.com/docker/distribution/blob/master/reference/regexp.go. # def container_repository_name_regex - @container_repository_regex ||= %r{\A[a-z0-9]+(?:[-._/][a-z0-9]+)*\Z} + @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-])[a-z0-9]+)*\Z} end ## diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb index 910533076b0..2c994536060 100644 --- a/lib/gitlab/routing.rb +++ b/lib/gitlab/routing.rb @@ -46,10 +46,10 @@ module Gitlab # Only replace the last occurence of `path`. # # `request.fullpath` includes the querystring - path = request.path.sub(%r{/#{path}/*(?!.*#{path})}, "/-/#{path}/") - path << "?#{request.query_string}" if request.query_string.present? + new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1") + new_path << "?#{request.query_string}" if request.query_string.present? - path + new_path end paths.each do |path| diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index e0a9d1dee77..d8faf7aad8c 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -28,6 +28,7 @@ module Gitlab def changed? return true unless gl_user + gl_user.changed? || gl_user.identities.any?(&:changed?) end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index a37112ae5c4..dc0184e4ad9 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -368,6 +368,7 @@ module Gitlab output, status = gitlab_shell_fast_execute_helper(cmd, vars) raise Error, output unless status.zero? + true end diff --git a/lib/gitlab/shell_adapter.rb b/lib/gitlab/shell_adapter.rb index fbe2a7a0d72..053dd4ab9e0 100644 --- a/lib/gitlab/shell_adapter.rb +++ b/lib/gitlab/shell_adapter.rb @@ -5,7 +5,7 @@ module Gitlab module ShellAdapter def gitlab_shell - Gitlab::Shell.new + @gitlab_shell ||= Gitlab::Shell.new end end end diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb index 11aeec1ebfa..f9faa134206 100644 --- a/lib/gitlab/string_range_marker.rb +++ b/lib/gitlab/string_range_marker.rb @@ -90,6 +90,7 @@ module Gitlab # Takes an array of integers, and returns an array of ranges covering the same integers def collapse_ranges(positions) return [] if positions.empty? + ranges = [] start = prev = positions[0] diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb index cb7957e2af9..33f07fa0120 100644 --- a/lib/gitlab/template/finders/repo_template_finder.rb +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -18,6 +18,7 @@ module Gitlab def read(path) blob = @repository.blob_at(@commit.id, path) if @commit raise FileNotFoundError if blob.nil? + blob.data end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 1caa791c1be..59331c827af 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -70,6 +70,7 @@ module Gitlab def generate_full_url return @url unless valid_credentials? + @full_url = @url.dup @full_url.password = credentials[:password] if credentials[:password].present? diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index c60bd91ea6e..11472ce6cce 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -99,6 +99,7 @@ module Gitlab def level_value(level) return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i) + string_options[level] || PRIVATE end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index e1219df1b25..864a9e04888 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -174,6 +174,7 @@ module Gitlab @secret ||= begin bytes = Base64.strict_decode64(File.read(secret_path).chomp) raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH + bytes end end diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb index 05668c69006..f5485eb89fa 100644 --- a/lib/haml_lint/inline_javascript.rb +++ b/lib/haml_lint/inline_javascript.rb @@ -9,6 +9,7 @@ unless Rails.env.production? def visit_filter(node) return unless node.filter_type == 'javascript' + record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)') end end diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index 00221f77cf4..8b145fb4511 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -24,6 +24,7 @@ module SystemCheck # @param [BaseCheck] check class def <<(check) raise ArgumentError unless check.is_a?(Class) && check < BaseCheck + @checks << check end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 87ca39b079b..c2d3a6b6950 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -1,5 +1,28 @@ namespace :gemojione do desc 'Generates Emoji SHA256 digests' + + task aliases: ['yarn:check', 'environment'] do + require 'json' + + aliases = {} + + index_file = File.join(Rails.root, 'fixtures', 'emojis', 'index.json') + index = JSON.parse(File.read(index_file)) + + index.each_pair do |key, data| + data['aliases'].each do |a| + a.tr!(':', '') + + aliases[a] = key + end + end + + out = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') + File.open(out, 'w') do |handle| + handle.write(JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: '')) + end + end + task digests: ['yarn:check', 'environment'] do require 'digest/sha2' require 'json' @@ -16,8 +39,13 @@ namespace :gemojione do fpath = File.join(dir, "#{emoji_hash['unicode']}.png") hash_digest = Digest::SHA256.file(fpath).hexdigest + category = emoji_hash['category'] + if name == 'gay_pride_flag' + category = 'flags' + end + entry = { - category: emoji_hash['category'], + category: category, moji: emoji_hash['moji'], description: emoji_hash['description'], unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name), @@ -29,7 +57,6 @@ namespace :gemojione do end out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') - File.open(out, 'w') do |handle| handle.write(JSON.pretty_generate(resultant_emoji_map)) end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 8ae1b6a626a..91c74bfb6b4 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -60,6 +60,7 @@ namespace :gitlab do .chomp('.git') .chomp('.wiki') next if Project.find_by_full_path(repo_with_namespace) + new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect File.rename(path, new_path) @@ -75,6 +76,7 @@ namespace :gitlab do User.find_each do |user| next unless user.ldap_user? + print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." if Gitlab::LDAP::Access.allowed?(user) puts " [OK]".color(:green) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 8377fe3269d..f2002d7a426 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -14,18 +14,18 @@ namespace :gitlab do checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) + command = %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + _, status = Gitlab::Popen.popen(%w[which gmake]) - command = status.zero? ? ['gmake'] : ['make'] + command << (status.zero? ? 'gmake' : 'make') - if Rails.env.test? - command += %W[BUNDLE_PATH=#{Bundler.bundle_path}] - end + command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? Dir.chdir(args.dir) do create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? - Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) } + Bundler.with_original_env { run_command!(command) } end end end @@ -82,9 +82,12 @@ namespace :gitlab do end def create_gitaly_configuration - File.open("config.toml", "w") do |f| + File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f| f.puts gitaly_configuration_toml end + rescue Errno::EEXIST + puts "Skipping config.toml generation:" + puts "A configuration file already exists." rescue ArgumentError => e puts "Skipping config.toml generation:" puts e.message diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake deleted file mode 100644 index 6cbc83b8973..00000000000 --- a/lib/tasks/gitlab/sidekiq.rake +++ /dev/null @@ -1,47 +0,0 @@ -namespace :gitlab do - namespace :sidekiq do - QUEUE = 'queue:post_receive'.freeze - - desc 'Drop all Sidekiq PostReceive jobs for a given project' - task :drop_post_receive, [:project] => :environment do |t, args| - unless args.project.present? - abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]" - end - project_path = Project.find_by_full_path(args.project).repository.path_to_repo - - Sidekiq.redis do |redis| - unless redis.exists(QUEUE) - abort "Queue #{QUEUE} is empty" - end - - temp_queue = "#{QUEUE}_#{Time.now.to_i}" - redis.rename(QUEUE, temp_queue) - - # At this point, then post_receive queue is empty. It may be receiving - # new jobs already. We will repopulate it with the old jobs, skipping the - # ones we want to drop. - dropped = 0 - while (job = redis.lpop(temp_queue)) - if repo_path(job) == project_path - dropped += 1 - else - redis.rpush(QUEUE, job) - end - end - # The temp_queue will delete itself after we have popped all elements - # from it - - puts "Dropped #{dropped} jobs containing #{project_path} from #{QUEUE}" - end - end - - def repo_path(job) - job_args = JSON.parse(job)['args'] - if job_args - job_args.first - else - nil - end - end - end -end diff --git a/package.json b/package.json index e607981143d..21e04724441 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "autosize": "^4.0.0", "axios": "^0.16.2", + "axios-mock-adapter": "^1.10.0", "babel-core": "^6.22.1", "babel-eslint": "^7.2.1", "babel-loader": "^7.1.1", @@ -74,6 +75,7 @@ "webpack-stats-plugin": "^0.1.5" }, "devDependencies": { + "@gitlab-org/gitlab-svgs": "^1.0.2", "babel-plugin-istanbul": "^4.0.0", "eslint": "^3.10.1", "eslint-config-airbnb-base": "^10.0.1", @@ -82,7 +84,6 @@ "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.5.0", - "gitlab-svgs": "https://gitlab.com/gitlab-org/gitlab-svgs.git", "istanbul": "^0.4.5", "jasmine-core": "^2.6.3", "jasmine-jquery": "^2.1.1", diff --git a/qa/Dockerfile b/qa/Dockerfile index f3a81a7e355..9b6ffff7c4d 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -9,6 +9,13 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list RUN apt-get update && apt-get install -y wget git unzip xvfb ## +# Install Docker +# +RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \ + tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \ + rm docker-17.09.0-ce.tgz + +## # Install Google Chrome version with headless support # RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add - diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb index ac939732b1d..ae6484b4bfe 100644 --- a/qa/qa/page/main/entry.rb +++ b/qa/qa/page/main/entry.rb @@ -16,6 +16,7 @@ module QA while Time.now - start < 240 break if page.has_css?('.application', wait: 10) + refresh end end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index ae099fd911e..b9d924651a0 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -8,6 +8,7 @@ module QA include Bootable def perform(address, *files) + Specs::Config.act { configure_capybara! } Runtime::Scenario.define(:gitlab_address, address) ## diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 9f9fe9844d2..bce7923e52d 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -9,6 +9,8 @@ require 'selenium-webdriver' module QA module Specs class Config < Scenario::Template + include Scenario::Actable + def perform configure_rspec! configure_capybara! diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index 92f91cb2725..1fde3f89a99 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -9,5 +9,16 @@ module QA expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) end end + + ## + # TODO, temporary workaround for gitlab-org/gitlab-qa#102. + # + after do + visit Runtime::Scenario.mattermost_address + reset_session! + + visit Runtime::Scenario.gitlab_address + reset_session! + end end end diff --git a/rubocop/cop/line_break_after_guard_clauses.rb b/rubocop/cop/line_break_after_guard_clauses.rb new file mode 100644 index 00000000000..67477f064ab --- /dev/null +++ b/rubocop/cop/line_break_after_guard_clauses.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Ensures a line break after guard clauses. + # + # @example + # # bad + # return unless condition + # do_stuff + # + # # good + # return unless condition + # + # do_stuff + # + # # bad + # raise if condition + # do_stuff + # + # # good + # raise if condition + # + # do_stuff + # + # Multiple guard clauses are allowed without + # line break. + # + # # good + # return unless condition_a + # return unless condition_b + # + # do_stuff + # + # Guard clauses in case statement are allowed without + # line break. + # + # # good + # case model + # when condition_a + # return true unless condition_b + # when + # ... + # end + # + # Guard clauses before end are allowed without + # line break. + # + # # good + # if condition_a + # do_something + # else + # do_something_else + # return unless condition + # end + # + # do_something_more + class LineBreakAfterGuardClauses < RuboCop::Cop::Cop + MSG = 'Add a line break after guard clauses' + + def_node_matcher :guard_clause_node?, <<-PATTERN + [{(send nil? {:raise :fail :throw} ...) return break next} single_line?] + PATTERN + + def on_if(node) + return unless node.single_line? + return unless guard_clause?(node) + return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node)) + + add_offense(node, :expression, MSG) + end + + def autocorrect(node) + lambda do |corrector| + corrector.insert_after(node.loc.expression, "\n") + end + end + + private + + def guard_clause?(node) + return false unless node.if_type? + + guard_clause_node?(node.if_branch) + end + + def next_sibling(node) + node.parent.children[node.sibling_index + 1] + end + + def next_line(node) + processed_source[node.loc.line] + end + + def clause_last_line?(line) + line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/ + end + end + end +end diff --git a/rubocop/cop/migration/add_column_with_default_to_large_table.rb b/rubocop/cop/migration/update_large_table.rb index fb363f95b56..3ae3fb1b68e 100644 --- a/rubocop/cop/migration/add_column_with_default_to_large_table.rb +++ b/rubocop/cop/migration/update_large_table.rb @@ -12,12 +12,12 @@ module RuboCop # # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more # information. - class AddColumnWithDefaultToLargeTable < RuboCop::Cop::Cop + class UpdateLargeTable < RuboCop::Cop::Cop include MigrationHelpers - MSG = 'Using `add_column_with_default` on the `%s` table will take a ' \ - 'long time to complete, and should be avoided unless absolutely ' \ - 'necessary'.freeze + MSG = 'Using `%s` on the `%s` table will take a long time to ' \ + 'complete, and should be avoided unless absolutely ' \ + 'necessary'.freeze LARGE_TABLES = %i[ ci_pipelines @@ -34,20 +34,22 @@ module RuboCop users ].freeze - def_node_matcher :add_column_with_default?, <<~PATTERN - (send nil :add_column_with_default $(sym ...) ...) + def_node_matcher :batch_update?, <<~PATTERN + (send nil ${:add_column_with_default :update_column_in_batches} $(sym ...) ...) PATTERN def on_send(node) return unless in_migration?(node) - matched = add_column_with_default?(node) - return unless matched + matches = batch_update?(node) + return unless matches + + update_method = matches.first + table = matches.last.to_a.first - table = matched.to_a.first return unless LARGE_TABLES.include?(table) - add_offense(node, :expression, format(MSG, table)) + add_offense(node, :expression, format(MSG, update_method, table)) end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 4ebbe010e90..7621ea50da9 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -3,11 +3,11 @@ require_relative 'cop/active_record_serialize' require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/in_batches' +require_relative 'cop/line_break_after_guard_clauses' require_relative 'cop/polymorphic_associations' require_relative 'cop/project_path_helper' require_relative 'cop/redirect_with_status' require_relative 'cop/migration/add_column' -require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' @@ -20,6 +20,7 @@ require_relative 'cop/migration/reversible_add_column_with_default' require_relative 'cop/migration/safer_boolean_column' require_relative 'cop/migration/timestamps' require_relative 'cop/migration/update_column_in_batches' +require_relative 'cop/migration/update_large_table' require_relative 'cop/rspec/env_assignment' require_relative 'cop/rspec/single_line_hook' require_relative 'cop/rspec/verbose_include_metadata' diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs index d3a9f5ff4ea..a270823b857 100755 --- a/scripts/trigger-build-docs +++ b/scripts/trigger-build-docs @@ -27,14 +27,7 @@ def docs_branch # Prefix the remote branch with 'preview-' in order to avoid # name conflicts in the rare case the branch name already # exists in the docs repo and truncate to max length. - "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max] -end - -# -# Dummy way to find out in which repo we are, CE or EE -# -def ee? - File.exist?('CHANGELOG-EE.md') + "#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max] end # @@ -56,14 +49,34 @@ def remove_remote_branch end # +# Define suffix in review app URL based on project +# +def slug + case ENV["CI_PROJECT_NAME"] + when 'gitlab-ce' + 'ce' + when 'gitlab-ee' + 'ee' + when 'gitlab-runner' + 'runner' + when 'omnibus-gitlab' + 'omnibus' + end +end + +# +# Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml +# +def param_name + "BRANCH_#{slug.upcase}" +end + +# # Trigger a pipeline in gitlab-docs # def trigger_pipeline - # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml - param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE' - # The review app URL - app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}" + app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{slug}" # Create the pipeline puts "=> Triggering a pipeline..." diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b73ca0c2346..768c7e99c96 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -6,6 +6,10 @@ describe ApplicationController do describe '#check_password_expiration' do let(:controller) { described_class.new } + before do + allow(controller).to receive(:session).and_return({}) + end + it 'redirects if the user is over their password expiry' do user.password_expires_at = Time.new(2002) diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index 4262d474e59..cb1b460fc0e 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -280,6 +280,17 @@ describe Groups::ChildrenController do expect(assigns(:children)).to contain_exactly(other_subgroup, *next_page_projects.take(per_page - 1)) end + + context 'with a mixed first page' do + let!(:first_page_subgroups) { [create(:group, :public, parent: group)] } + let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) } + + it 'correctly calculates the counts' do + get :index, group_id: group.to_param, sort: 'id_asc', page: 2, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end end end end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 5dc27e2bbba..fd90c0d8bad 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Projects::CommitController do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } let(:commit) { project.commit("master") } let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } let(:master_pickable_commit) { project.commit(master_pickable_sha) } before do sign_in(user) - project.team << [user, :master] + project.add_master(user) end describe 'GET show' do diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 5f5a789d5cc..37e9f863fc4 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -336,6 +336,29 @@ describe Projects::NotesController do end end + describe 'PUT update' do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :json, + note: { + note: "New comment" + } + } + end + + before do + sign_in(note.author) + project.team << [note.author, :developer] + end + + it "updates the note" do + expect { put :update, request_params }.to change { note.reload.note } + end + end + describe 'DELETE destroy' do let(:request_params) do { diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb new file mode 100644 index 00000000000..509c4e1fa1c --- /dev/null +++ b/spec/factories/fork_network_members.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :fork_network_member do + association :project + association :fork_network + + forked_from_project { fork_network.root_project } + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index f0d05504b7e..ab4ae123429 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -130,6 +130,7 @@ FactoryGirl.define do before(:create) do |note, evaluator| discussion = evaluator.in_reply_to next unless discussion + discussion = discussion.to_discussion if discussion.is_a?(Note) next unless discussion diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index b47f9055d29..a69b428d117 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -167,19 +167,36 @@ describe "Admin::Users" do it 'sees impersonation log out icon' do icon = first('.fa.fa-user-secret') - expect(icon).not_to eql nil + expect(icon).not_to be nil end it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do find(:css, 'li.impersonation a').click - expect(current_path).to eql "/admin/users/#{another_user.username}" + expect(current_path).to eq("/admin/users/#{another_user.username}") + end + end + + context 'when impersonating a user with an expired password' do + before do + another_user.update(password_expires_at: Time.now - 5.minutes) + click_link 'Impersonate' + end + + it 'does not redirect to password change page' do + expect(current_path).to eq('/') + end + + it 'is redirected back to the impersonated users page in the admin after stopping' do + find(:css, 'li.impersonation a').click + + expect(current_path).to eq("/admin/users/#{another_user.username}") end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 9137ab82ff4..205900615c4 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -331,11 +331,29 @@ describe 'Issue Boards', :js do context 'subscription' do it 'changes issue subscription' do click_card(card) + wait_for_requests - page.within('.subscription') do + page.within('.subscriptions') do click_button 'Subscribe' wait_for_requests - expect(page).to have_content("Unsubscribe") + + expect(page).to have_content('Unsubscribe') + end + end + + it 'has "Unsubscribe" button when already subscribed' do + create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true) + visit project_board_path(project, board) + wait_for_requests + + click_card(card) + wait_for_requests + + page.within('.subscriptions') do + click_button 'Unsubscribe' + wait_for_requests + + expect(page).to have_content('Subscribe') end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 479fb713297..98586ddbd81 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Commits' do - include CiStatusHelper - let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -33,7 +31,7 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it { expect(page).to have_content pipeline.sha[0..7] } @@ -79,7 +77,7 @@ describe 'Commits' do describe 'Commit builds', :js do before do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'shows pipeline`s data' do @@ -95,7 +93,7 @@ describe 'Commits' do end it do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) click_on 'Download artifacts' expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end @@ -103,7 +101,7 @@ describe 'Commits' do describe 'Cancel all builds' do it 'cancels commit', :js do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) click_on 'Cancel running' expect(page).to have_content 'canceled' end @@ -111,7 +109,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build', :js do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) find('.js-btn-cancel-pipeline').click expect(page).to have_content 'canceled' end @@ -120,13 +118,13 @@ describe 'Commits' do describe '.gitlab-ci.yml not found warning' do context 'ci builds enabled' do it "does not show warning" do - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end it 'shows warning' do stub_ci_pipeline_yaml_file(nil) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) expect(page).to have_content '.gitlab-ci.yml not found in this commit' end end @@ -135,7 +133,7 @@ describe 'Commits' do before do stub_ci_builds_disabled stub_ci_pipeline_yaml_file(nil) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'does not show warning' do @@ -149,7 +147,7 @@ describe 'Commits' do before do project.team << [user, :reporter] build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it 'Renders header', :js do @@ -171,7 +169,7 @@ describe 'Commits' do visibility_level: Gitlab::VisibilityLevel::INTERNAL, public_builds: false) build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(pipeline) + visit pipeline_path(pipeline) end it do @@ -202,5 +200,12 @@ describe 'Commits' do expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") end end + + it 'shows the ref switcher with the multi-file editor enabled', :js do + set_cookie('new_repo', 'true') + visit project_commits_path(project, branch_name) + + expect(find('.js-project-refs-dropdown')).to have_content branch_name + end end end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index a55ecaa5697..b579e32c9aa 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -13,6 +13,8 @@ "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, "relative_position": { "type": "integer" }, + "issue_sidebar_endpoint": { "type": "string" }, + "toggle_subscription_endpoint": { "type": "string" }, "project": { "id": { "type": "integer" }, "path": { "type": "string" } diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index d7b66e6f078..c358ccae9c3 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -1,10 +1,36 @@ require 'spec_helper' describe TreeHelper do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } + + describe '.render_tree' do + before do + @id = sha + @project = project + end + + it 'displays all entries without a warning' do + tree = repository.tree(sha, 'files') + + html = render_tree(tree) + + expect(html).not_to have_selector('.tree-truncated-warning') + end + + it 'truncates entries and adds a warning' do + stub_const('TreeHelper::FILE_LIMIT', 1) + tree = repository.tree(sha, 'files') + + html = render_tree(tree) + + expect(html).to have_selector('.tree-truncated-warning', count: 1) + expect(html).to have_selector('.tree-item-file-name', count: 1) + end + end + describe 'flatten_tree' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' } let(:tree) { repository.tree(sha, 'files') } let(:root_path) { 'files' } let(:tree_item) { tree.entries.find { |entry| entry.path == path } } diff --git a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js index ec2c549e032..f96f20ed4a5 100644 --- a/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js +++ b/spec/javascripts/behaviors/gl_emoji/unicode_support_map_spec.js @@ -21,13 +21,18 @@ describe('Unicode Support Map', () => { }); it('should call .getItem and .setItem', () => { - const allArgs = window.localStorage.setItem.calls.allArgs(); - - expect(window.localStorage.getItem).toHaveBeenCalledWith('gl-emoji-user-agent'); - expect(allArgs[0][0]).toBe('gl-emoji-user-agent'); - expect(allArgs[0][1]).toBe(navigator.userAgent); - expect(allArgs[1][0]).toBe('gl-emoji-unicode-support-map'); - expect(allArgs[1][1]).toBe(stringSupportMap); + const getArgs = window.localStorage.getItem.calls.allArgs(); + const setArgs = window.localStorage.setItem.calls.allArgs(); + + expect(getArgs[0][0]).toBe('gl-emoji-version'); + expect(getArgs[1][0]).toBe('gl-emoji-user-agent'); + + expect(setArgs[0][0]).toBe('gl-emoji-version'); + expect(setArgs[0][1]).toBe('0.2.0'); + expect(setArgs[1][0]).toBe('gl-emoji-user-agent'); + expect(setArgs[1][1]).toBe(navigator.userAgent); + expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map'); + expect(setArgs[2][1]).toBe(stringSupportMap); }); }); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 83b13b06dc1..8f607899b20 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,10 +9,11 @@ import Vue from 'vue'; import '~/boards/models/assignee'; +import eventHub from '~/boards/eventhub'; import '~/boards/models/list'; import '~/boards/models/label'; import '~/boards/stores/boards_store'; -import boardCard from '~/boards/components/board_card'; +import boardCard from '~/boards/components/board_card.vue'; import './mock_data'; describe('Board card', () => { @@ -157,33 +158,35 @@ describe('Board card', () => { }); it('sets detail issue to card issue on mouse up', () => { + spyOn(eventHub, '$emit'); + triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue); expect(gl.issueBoards.BoardsStore.detail.list).toEqual(vm.list); }); it('adds active class if detail issue is set', (done) => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); - - setTimeout(() => { - expect(vm.$el.classList.contains('is-active')).toBe(true); - done(); - }, 0); + vm.detailIssue.issue = vm.issue; + + Vue.nextTick() + .then(() => { + expect(vm.$el.classList.contains('is-active')).toBe(true); + }) + .then(done) + .catch(done.fail); }); it('resets detail issue to empty if already set', () => { - triggerEvent('mousedown'); - triggerEvent('mouseup'); + spyOn(eventHub, '$emit'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual(vm.issue); + gl.issueBoards.BoardsStore.detail.issue = vm.issue; triggerEvent('mousedown'); triggerEvent('mouseup'); - expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); + expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue'); }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 022d286d5df..ccde657789a 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -133,6 +133,19 @@ describe('Issue model', () => { expect(relativePositionIssue.position).toBe(1); }); + it('updates data', () => { + issue.updateData({ subscribed: true }); + expect(issue.subscribed).toBe(true); + }); + + it('sets fetching state', () => { + expect(issue.isFetching.subscriptions).toBe(true); + + issue.setFetchingState('subscriptions', false); + + expect(issue.isFetching.subscriptions).toBe(false); + }); + describe('update', () => { it('passes assignee ids when there are assignees', (done) => { spyOn(Vue.http, 'patch').and.callFake((url, data) => { diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js index fa11c602ec3..124d91f4477 100644 --- a/spec/javascripts/emoji_spec.js +++ b/spec/javascripts/emoji_spec.js @@ -1,6 +1,7 @@ import { glEmojiTag } from '~/emoji'; import isEmojiUnicodeSupported, { isFlagEmoji, + isRainbowFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, isHorceRacingSkinToneComboEmoji, @@ -217,6 +218,24 @@ describe('gl_emoji', () => { }); }); + describe('isRainbowFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isRainbowFlagEmoji('')).toBeFalsy(); + }); + it('should detect rainbow_flag', () => { + expect(isRainbowFlagEmoji('🏳🌈')).toBeTruthy(); + }); + it('should not detect flag_white on its\' own', () => { + expect(isRainbowFlagEmoji('🏳')).toBeFalsy(); + }); + it('should not detect rainbow on its\' own', () => { + expect(isRainbowFlagEmoji('🌈')).toBeFalsy(); + }); + it('should not detect flag_white with something else', () => { + expect(isRainbowFlagEmoji('🏳🔵')).toBeFalsy(); + }); + }); + describe('isKeycapEmoji', () => { it('should gracefully handle empty string', () => { expect(isKeycapEmoji('')).toBeFalsy(); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 2ea290108a4..5662c7387fb 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -223,23 +223,46 @@ describe('Issuable output', () => { }); }); - it('closes form on error', (done) => { - spyOn(window, 'Flash').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { - reject(); - })); + describe('error when updating', () => { + beforeEach(() => { + spyOn(window, 'Flash').and.callThrough(); + spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { + reject(); + })); + }); - vm.updateIssuable(); + it('closes form on error', (done) => { + vm.updateIssuable(); - setTimeout(() => { - expect( - eventHub.$emit, - ).toHaveBeenCalledWith('close.form'); - expect( - window.Flash, - ).toHaveBeenCalledWith('Error updating issue'); + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating issue'); - done(); + done(); + }); + }); + + it('returns the correct error message for issuableType', (done) => { + vm.issuableType = 'merge request'; + + Vue.nextTick(() => { + vm.updateIssuable(); + + setTimeout(() => { + expect( + eventHub.$emit, + ).toHaveBeenCalledWith('close.form'); + expect( + window.Flash, + ).toHaveBeenCalledWith('Error updating merge request'); + + done(); + }); + }); }); }); }); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js index f6625b748b6..d779ab7bb31 100644 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -61,6 +61,15 @@ describe('Edit Actions components', () => { }); }); + it('should not show delete button if showDeleteButton is false', (done) => { + vm.showDeleteButton = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + done(); + }); + }); + describe('updateIssuable', () => { it('sends update.issauble event when clicking save button', () => { vm.$el.querySelector('.btn-save').click(); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js index 1d7fa7e12fc..3069a0cd60e 100644 --- a/spec/javascripts/jobs/job_details_mediator_spec.js +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -1,39 +1,35 @@ -import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import JobMediator from '~/jobs/job_details_mediator'; import job from './mock_data'; describe('JobMediator', () => { let mediator; + let mock; beforeEach(() => { - mediator = new JobMediator({ endpoint: 'foo' }); + mediator = new JobMediator({ endpoint: 'jobs/40291672.json' }); + mock = new MockAdapter(axios); }); it('should set defaults', () => { expect(mediator.store).toBeDefined(); expect(mediator.service).toBeDefined(); - expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' }); expect(mediator.state.isLoading).toEqual(false); }); describe('request and store data', () => { - const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(job), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); + mock.onGet().reply(200, job, {}); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + mock.restore(); }); it('should store received data', (done) => { mediator.fetchJob(); - setTimeout(() => { expect(mediator.store.state.job).toEqual(job); done(); diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js index 8ece913ada8..c83bd19345f 100644 --- a/spec/javascripts/monitoring/graph_path_spec.js +++ b/spec/javascripts/monitoring/graph_path_spec.js @@ -32,4 +32,21 @@ describe('Monitoring Paths', () => { expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); }); + + describe('Computed properties', () => { + it('strokeDashArray', () => { + const component = createComponent({ + generatedLinePath: firstTimeSeries.linePath, + generatedAreaPath: firstTimeSeries.areaPath, + lineColor: firstTimeSeries.lineColor, + areaColor: firstTimeSeries.areaColor, + }); + + component.lineStyle = 'dashed'; + expect(component.strokeDashArray).toBe('3, 1'); + + component.lineStyle = 'dotted'; + expect(component.strokeDashArray).toBe('1, 1'); + }); + }); }); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index c57f44dae17..50a5e4ff056 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ -/* global NewBranchForm */ -import '~/new_branch_form'; +import NewBranchForm from '~/new_branch_form'; (function() { describe('Branch', function() { diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 0795d0aaa82..1ad7c2d8efa 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -202,7 +202,6 @@ export default { "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", "email_patches_path": "/root/acets-app/merge_requests/22.patch", "plain_diff_path": "/root/acets-app/merge_requests/22.diff", - "ci_status_path": "/root/acets-app/merge_requests/22/ci_status", "status_path": "/root/acets-app/merge_requests/22.json", "merge_check_path": "/root/acets-app/merge_requests/22/merge_check", "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status", diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js index 2cf4d8e00ed..24484796bf1 100644 --- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js +++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js @@ -16,7 +16,7 @@ describe('Issue Warning Component', () => { isLocked: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.'); }); }); @@ -27,7 +27,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash'); + expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.'); }); }); @@ -39,7 +39,7 @@ describe('Issue Warning Component', () => { isConfidential: true, }); - expect(vm.$el.querySelector('i')).toBeFalsy(); + expect(vm.$el.querySelector('.icon')).toBeFalsy(); expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.'); }); }); diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js index 97c8a08fcdd..49bf8ee6f7c 100644 --- a/spec/javascripts/vue_shared/components/loading_button_spec.js +++ b/spec/javascripts/vue_shared/components/loading_button_spec.js @@ -66,6 +66,23 @@ describe('LoadingButton', function () { }); }); + describe('container class', () => { + it('should default to btn btn-align-content', () => { + vm = mountComponent(LoadingButton, {}); + expect(vm.$el.classList.contains('btn')).toEqual(true); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(true); + }); + + it('should be configurable through props', () => { + vm = mountComponent(LoadingButton, { + containerClass: 'test-class', + }); + expect(vm.$el.classList.contains('btn')).toEqual(false); + expect(vm.$el.classList.contains('btn-align-content')).toEqual(false); + expect(vm.$el.classList.contains('test-class')).toEqual(true); + }); + }); + describe('click callback prop', () => { it('calls given callback when normal', () => { vm = mountComponent(LoadingButton, { @@ -81,7 +98,6 @@ describe('LoadingButton', function () { it('does not call given callback when disabled because of loading', () => { vm = mountComponent(LoadingButton, { loading: true, - indeterminate: true, }); spyOn(vm, '$emit'); diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js new file mode 100644 index 00000000000..818ef0af3c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import toolbar from '~/vue_shared/components/markdown/toolbar.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('toolbar', () => { + let vm; + const Toolbar = Vue.extend(toolbar); + const props = { + markdownDocsPath: '', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('user can attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, props); + }); + + it('should render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).not.toBeNull(); + }); + }); + + describe('user cannot attach file', () => { + beforeEach(() => { + vm = mountComponent(Toolbar, Object.assign({}, props, { + canAttachFile: false, + })); + }); + + it('should not render uploading-container', () => { + expect(vm.$el.querySelector('.uploading-container')).toBeNull(); + }); + }); +}); diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb index 84cacdd3f0d..010deae822c 100644 --- a/spec/lib/container_registry/path_spec.rb +++ b/spec/lib/container_registry/path_spec.rb @@ -86,6 +86,24 @@ describe ContainerRegistry::Path do it { is_expected.to be_valid } end + + context 'when path contains double underscore' do + let(:path) { 'my/repository__name' } + + it { is_expected.to be_valid } + end + + context 'when path contains invalid separator with dot' do + let(:path) { 'some/registry-.name' } + + it { is_expected.not_to be_valid } + end + + context 'when path contains invalid separator with underscore' do + let(:path) { 'some/registry._name' } + + it { is_expected.not_to be_valid } + end end describe '#has_repository?' do diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb new file mode 100644 index 00000000000..ffcd90b9fcb --- /dev/null +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Auth::RequestAuthenticator do + let(:env) do + { + 'rack.input' => '', + 'REQUEST_METHOD' => 'GET' + } + end + let(:request) { ActionDispatch::Request.new(env) } + + subject { described_class.new(request) } + + describe '#user' do + let!(:sessionless_user) { build(:user) } + let!(:session_user) { build(:user) } + + it 'returns sessionless user first' do + allow_any_instance_of(described_class).to receive(:find_sessionless_user).and_return(sessionless_user) + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) + + expect(subject.user).to eq sessionless_user + end + + it 'returns session user if no sessionless user found' do + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_return(session_user) + + expect(subject.user).to eq session_user + end + + it 'returns nil if no user found' do + expect(subject.user).to be_blank + end + + it 'bubbles up exceptions' do + allow_any_instance_of(described_class).to receive(:find_user_from_warden).and_raise(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#find_sessionless_user' do + let!(:access_token_user) { build(:user) } + let!(:rss_token_user) { build(:user) } + + it 'returns access_token user first' do + allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_return(access_token_user) + allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + + expect(subject.find_sessionless_user).to eq access_token_user + end + + it 'returns rss_token user if no access_token user found' do + allow_any_instance_of(described_class).to receive(:find_user_from_rss_token).and_return(rss_token_user) + + expect(subject.find_sessionless_user).to eq rss_token_user + end + + it 'returns nil if no user found' do + expect(subject.find_sessionless_user).to be_blank + end + + it 'rescue Gitlab::Auth::AuthenticationError exceptions' do + allow_any_instance_of(described_class).to receive(:find_user_from_access_token).and_raise(Gitlab::Auth::UnauthorizedError) + + expect(subject.find_sessionless_user).to be_blank + end + end +end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb new file mode 100644 index 00000000000..4637816570c --- /dev/null +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -0,0 +1,194 @@ +require 'spec_helper' + +describe Gitlab::Auth::UserAuthFinders do + include described_class + + let(:user) { create(:user) } + let(:env) do + { + 'rack.input' => '' + } + end + let(:request) { Rack::Request.new(env)} + + def set_param(key, value) + request.update_param(key, value) + end + + describe '#find_user_from_warden' do + context 'with CSRF token' do + before do + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true) + end + + context 'with invalid credentials' do + it 'returns nil' do + expect(find_user_from_warden).to be_nil + end + end + + context 'with valid credentials' do + it 'returns the user' do + env['warden'] = double("warden", authenticate: user) + + expect(find_user_from_warden).to eq user + end + end + end + + context 'without CSRF token' do + it 'returns nil' do + allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false) + env['warden'] = double("warden", authenticate: user) + + expect(find_user_from_warden).to be_nil + end + end + end + + describe '#find_user_from_rss_token' do + context 'when the request format is atom' do + before do + env['HTTP_ACCEPT'] = 'application/atom+xml' + end + + it 'returns user if valid rss_token' do + set_param(:rss_token, user.rss_token) + + expect(find_user_from_rss_token).to eq user + end + + it 'returns nil if rss_token is blank' do + expect(find_user_from_rss_token).to be_nil + end + + it 'returns exception if invalid rss_token' do + set_param(:rss_token, 'invalid_token') + + expect { find_user_from_rss_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + context 'when the request format is not atom' do + it 'returns nil' do + set_param(:rss_token, user.rss_token) + + expect(find_user_from_rss_token).to be_nil + end + end + end + + describe '#find_user_from_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'returns nil if no access_token present' do + expect(find_personal_access_token).to be_nil + end + + context 'when validate_access_token! returns valid' do + it 'returns user' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect(find_user_from_access_token).to eq user + end + + it 'returns exception if token has no user' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil) + + expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + end + + describe '#find_personal_access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + context 'passed as header' do + it 'returns token if valid personal_access_token' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect(find_personal_access_token).to eq personal_access_token + end + end + + context 'passed as param' do + it 'returns token if valid personal_access_token' do + set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, personal_access_token.token) + + expect(find_personal_access_token).to eq personal_access_token + end + end + + it 'returns nil if no personal_access_token' do + expect(find_personal_access_token).to be_nil + end + + it 'returns exception if invalid personal_access_token' do + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid_token' + + expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#find_oauth_access_token' do + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + + context 'passed as header' do + it 'returns token if valid oauth_access_token' do + env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}" + + expect(find_oauth_access_token.token).to eq token.token + end + end + + context 'passed as param' do + it 'returns user if valid oauth_access_token' do + set_param(:access_token, token.token) + + expect(find_oauth_access_token.token).to eq token.token + end + end + + it 'returns nil if no oauth_access_token' do + expect(find_oauth_access_token).to be_nil + end + + it 'returns exception if invalid oauth_access_token' do + env['HTTP_AUTHORIZATION'] = "Bearer invalid_token" + + expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) + end + end + + describe '#validate_access_token!' do + let(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'returns nil if no access_token present' do + expect(validate_access_token!).to be_nil + end + + context 'token is not valid' do + before do + allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token) + end + + it 'returns Gitlab::Auth::ExpiredError if token expired' do + personal_access_token.expires_at = 1.day.ago + + expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError) + end + + it 'returns Gitlab::Auth::RevokedError if token revoked' do + personal_access_token.revoke! + + expect { validate_access_token! }.to raise_error(Gitlab::Auth::RevokedError) + end + + it 'returns Gitlab::Auth::InsufficientScopeError if invalid token scope' do + expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index 2c2684a6fc9..994992f79d4 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -3,12 +3,9 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } let(:base1) { create(:project) } - let(:base1_fork1) { create(:project) } - let(:base1_fork2) { create(:project) } let(:base2) { create(:project) } let(:base2_fork1) { create(:project) } - let(:base2_fork2) { create(:project) } let!(:forked_project_links) { table(:forked_project_links) } let!(:fork_networks) { table(:fork_networks) } @@ -21,21 +18,24 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # A normal fork link forked_project_links.create(id: 1, forked_from_project_id: base1.id, - forked_to_project_id: base1_fork1.id) + forked_to_project_id: create(:project).id) forked_project_links.create(id: 2, forked_from_project_id: base1.id, - forked_to_project_id: base1_fork2.id) - + forked_to_project_id: create(:project).id) forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + + # create a fork of a fork forked_project_links.create(id: 4, forked_from_project_id: base2_fork1.id, forked_to_project_id: create(:project).id) - forked_project_links.create(id: 5, - forked_from_project_id: base2.id, - forked_to_project_id: base2_fork2.id) + forked_from_project_id: create(:project).id, + forked_to_project_id: create(:project).id) + + # Stub out the calls to the other migrations + allow(BackgroundMigrationWorker).to receive(:perform_in) migration.perform(1, 3) end @@ -80,11 +80,11 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch end it 'only processes a single batch of links at a time' do - expect(fork_network_members.count).to eq(5) + expect(fork_networks.count).to eq(2) migration.perform(3, 5) - expect(fork_network_members.count).to eq(7) + expect(fork_networks.count).to eq(3) end it 'can be repeated without effect' do diff --git a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb index 4ea7f441f7c..0cb753c5853 100644 --- a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_requests_latest_merge_request_diff_id_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20171026082505_populate_merge_requests_latest_merge_request_diff_id') -describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do +describe Gitlab::BackgroundMigration::PopulateMergeRequestsLatestMergeRequestDiffId, :migration, schema: 20171026082505 do let(:projects_table) { table(:projects) } let(:merge_requests_table) { table(:merge_requests) } let(:merge_request_diffs_table) { table(:merge_request_diffs) } @@ -27,30 +26,32 @@ describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do merge_request_diffs_table.where(merge_request_id: merge_request.id) end - describe '#up' do + describe '#perform' do it 'ignores MRs without diffs' do merge_request_without_diff = create_mr!('without_diff') + mr_id = merge_request_without_diff.id expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil - expect { migrate! } + expect { subject.perform(mr_id, mr_id) } .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id } end it 'ignores MRs that have a diff ID already set' do merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3) diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id) + mr_id = merge_request_with_multiple_diffs.id merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id) - expect { migrate! } + expect { subject.perform(mr_id, mr_id) } .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id } end it 'migrates multiple MR diffs to the correct values' do merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) } - migrate! + subject.perform(merge_requests.first.id, merge_requests.last.id) merge_requests.each do |merge_request| expect(merge_request.reload.latest_merge_request_diff_id) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index a66347ead76..a6a1d9e619f 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -54,11 +54,13 @@ describe Gitlab::BitbucketImport::Importer do create( :project, import_source: project_identifier, + import_url: "https://bitbucket.org/#{project_identifier}.git", import_data_attributes: { credentials: data } ) end let(:importer) { described_class.new(project) } + let(:gitlab_shell) { double } let(:issues_statuses_sample_data) do { @@ -67,6 +69,10 @@ describe Gitlab::BitbucketImport::Importer do } end + before do + allow(importer).to receive(:gitlab_shell) { gitlab_shell } + end + context 'issues statuses' do before do # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this @@ -110,15 +116,36 @@ describe Gitlab::BitbucketImport::Importer do end it 'maps statuses to open or closed' do + allow(importer).to receive(:import_wiki) + importer.execute expect(project.issues.where(state: "closed").size).to eq(5) expect(project.issues.where(state: "opened").size).to eq(2) end - it 'calls import_wiki' do - expect(importer).to receive(:import_wiki) - importer.execute + describe 'wiki import' do + it 'is skipped when the wiki exists' do + expect(project.wiki).to receive(:repository_exists?) { true } + expect(importer.gitlab_shell).not_to receive(:import_repository) + + importer.execute + + expect(importer.errors).to be_empty + end + + it 'imports to the project disk_path' do + expect(project.wiki).to receive(:repository_exists?) { false } + expect(importer.gitlab_shell).to receive(:import_repository).with( + project.repository_storage_path, + project.wiki.disk_path, + project.import_url + '/wiki' + ) + + importer.execute + + expect(importer.errors).to be_empty + end end end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index bf981d2f6f6..92792144429 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -84,6 +84,13 @@ describe Gitlab::Conflict::File do expect(line.text).to eq(html_to_text(line.rich_text)) end end + + # This spec will break if Rouge's highlighting changes, but we need to + # ensure that the lines are actually highlighted. + it 'highlights the lines correctly' do + expect(conflict_file.lines.first.rich_text) + .to eq("<span id=\"LC1\" class=\"line\" lang=\"ruby\"><span class=\"k\">module</span> <span class=\"nn\">Gitlab</span></span>\n") + end end describe '#sections' do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 8922370b0a0..e850b5cd6a4 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -87,6 +87,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end + it 'does not move the repositories when hashed storage is enabled' do + project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) + + expect(subject).not_to receive(:move_repository) + + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') + end + it 'moves uploads' do expect(subject).to receive(:move_uploads) .with('known-parent/the-path', 'known-parent/the-path0') @@ -94,6 +102,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') end + it 'does not move uploads when hashed storage is enabled for attachments' do + project.update!(storage_version: Project::HASHED_STORAGE_FEATURES[:attachments]) + + expect(subject).not_to receive(:move_uploads) + + subject.move_project_folders(project, 'known-parent/the-path', 'known-parent/the-path0') + end + it 'moves pages' do expect(subject).to receive(:move_pages) .with('known-parent/the-path', 'known-parent/the-path0') diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index c91895cedc3..ff9acfd08b9 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -116,12 +116,8 @@ describe Gitlab::Diff::File do end context 'when renamed' do - let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } - let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } - - before do - allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id) - end + let(:commit) { project.commit('94bb47ca1297b7b3731ff2a36923640991e9236f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('CHANGELOG.md') } it 'returns false' do expect(diff_file.content_changed?).to be_falsey diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index ee657101f4c..65edc750f39 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -487,6 +487,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do loop do break if @count.zero? + # It is critical to decrement before yielding. We may never reach the lines after 'yield'. @count -= 1 yield @value diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5d990b42c24..f0da77c61bb 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -629,16 +629,29 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#remote_tags' do + let(:remote_name) { 'upstream' } let(:target_commit_id) { SeedRepo::Commit::ID } + let(:user) { create(:user) } + let(:tag_name) { 'v0.0.1' } + let(:tag_message) { 'My tag' } + let(:remote_repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end - subject { repository.remote_tags('upstream') } + subject { repository.remote_tags(remote_name) } - it 'gets the remote tags' do - expect(repository).to receive(:list_remote_tags).with('upstream') - .and_return(["#{target_commit_id}\trefs/tags/v0.0.1\n"]) + before do + repository.add_remote(remote_name, remote_repository.path) + remote_repository.add_tag(tag_name, user: user, target: target_commit_id) + end + + after do + ensure_seeds + end + it 'gets the remote tags' do expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) - expect(subject.first.name).to eq('v0.0.1') + expect(subject.first.name).to eq(tag_name) expect(subject.first.dereferenced_target.id).to eq(target_commit_id) end end @@ -1770,6 +1783,32 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#delete_all_refs_except' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + + before do + repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") + repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1") + repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae") + repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9") + end + + after do + ensure_seeds + end + + it 'deletes all refs except those with the specified prefixes' do + repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads)) + expect(repository.ref_exists?("refs/delete/a")).to be(false) + expect(repository.ref_exists?("refs/also-delete/b")).to be(false) + expect(repository.ref_exists?("refs/keep/c")).to be(true) + expect(repository.ref_exists?("refs/also-keep/d")).to be(true) + expect(repository.ref_exists?("refs/heads/master")).to be(true) + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 8127b4842b7..951e146a30a 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -104,4 +104,17 @@ describe Gitlab::GitalyClient::RefService do expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError) end end + + describe '#delete_refs' do + let(:prefixes) { %w(refs/heads refs/keep-around) } + + it 'sends a delete_refs message' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:delete_refs) + .with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash)) + .and_return(double('delete_refs_response')) + + client.delete_refs(except_with_prefixes: prefixes) + end + end end diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb index 30da56bec16..26529c4759d 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -41,7 +41,8 @@ describe Gitlab::HookData::IssuableBuilder do labels: [ [{ id: 1, title: 'foo' }], [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }] - ] + ], + total_time_spent: [1, 2] } end let(:data) { builder.build(user: user, changes: changes) } @@ -53,6 +54,10 @@ describe Gitlab::HookData::IssuableBuilder do labels: { previous: [{ id: 1, title: 'foo' }], current: [{ id: 1, title: 'foo' }, { id: 2, title: 'bar' }] + }, + total_time_spent: { + previous: 1, + current: 2 } })) end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index e4b4cf5ba85..c2bda6f8821 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -155,7 +155,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end it 'has no source if source/target differ' do - expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1) + expect(MergeRequest.find_by_title('MR2').source_project_id).to be_nil end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 67121937398..60a134be939 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -127,6 +127,14 @@ describe Gitlab::Middleware::Go do include_examples 'go-get=1', enabled_protocol: nil end + + context 'with nothing disabled (blank string)' do + before do + stub_application_setting(enabled_git_access_protocol: '') + end + + include_examples 'go-get=1', enabled_protocol: nil + end end def go diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index b14735943a5..07ba11b93a3 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -84,14 +84,23 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') expect(response).to be_a_redirect expect(subject).to disallow_request end + it 'returns last_vistited_url for disallowed request' do + response = request.post('/test_request') + + expect(response.location).to eq 'http://localhost/' + end + context 'whitelisted requests' do it 'expects a POST internal request to be allowed' do + expect(Rails.application.routes).not_to receive(:recognize_path) + response = request.post("/api/#{API::API.version}/internal") expect(response).not_to be_a_redirect @@ -99,6 +108,7 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects a POST LFS request to batch URL to be allowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/rouge.git/info/lfs/objects/batch') expect(response).not_to be_a_redirect @@ -106,6 +116,7 @@ describe Gitlab::Middleware::ReadOnly do end it 'expects a POST request to git-upload-pack URL to be allowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original response = request.post('/root/rouge.git/git-upload-pack') expect(response).not_to be_a_redirect diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index c7471a21fda..2f19fb7312d 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -662,4 +662,13 @@ describe Gitlab::OAuth::User do end end end + + describe '.find_by_uid_and_provider' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + it 'normalizes extern_uid' do + allow(oauth_user.auth_hash).to receive(:uid).and_return('MY-UID') + expect(oauth_user.find_user).to eql gl_user + end + end end diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb new file mode 100644 index 00000000000..cf6ae5cda74 --- /dev/null +++ b/spec/migrations/remove_empty_fork_networks_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171114104051_remove_empty_fork_networks.rb') + +describe RemoveEmptyForkNetworks, :migration do + let!(:fork_networks) { table(:fork_networks) } + + let(:deleted_project) { create(:project) } + let!(:empty_network) { create(:fork_network, id: 1, root_project_id: deleted_project.id) } + let!(:other_network) { create(:fork_network, id: 2, root_project_id: create(:project).id) } + + before do + deleted_project.destroy! + end + + it 'deletes only the fork network without members' do + expect(fork_networks.count).to eq(2) + + migrate! + + expect(fork_networks.find_by(id: empty_network.id)).to be_nil + expect(fork_networks.find_by(id: other_network.id)).not_to be_nil + expect(fork_networks.count).to eq(1) + end +end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb index f95bd6e3511..76afb6c19cf 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb @@ -2,19 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170703130158_schedule_merge_request_diff_migrations') describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do - matcher :be_scheduled_migration do |time, *expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] && - job['at'].to_i == time.to_i - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } let(:projects) { table(:projects) } @@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes.from_now, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes.from_now, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb index 4ab1bb67058..cf323973384 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb @@ -2,19 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170926150348_schedule_merge_request_diff_migrations_take_two') describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do - matcher :be_scheduled_migration do |time, *expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] && - job['at'].to_i == time.to_i - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } let(:projects) { table(:projects) } @@ -37,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes.from_now, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes.from_now, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb new file mode 100644 index 00000000000..158d0bc02ed --- /dev/null +++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171026082505_schedule_merge_request_latest_merge_request_diff_id_migrations') + +describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :sidekiq do + let(:projects_table) { table(:projects) } + let(:merge_requests_table) { table(:merge_requests) } + let(:merge_request_diffs_table) { table(:merge_request_diffs) } + + let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') } + + let!(:merge_request_1) { create_mr!('mr_1', diffs: 1) } + let!(:merge_request_2) { create_mr!('mr_2', diffs: 2) } + let!(:merge_request_migrated) { create_mr!('merge_request_migrated', diffs: 3) } + let!(:merge_request_4) { create_mr!('mr_4', diffs: 3) } + + def create_mr!(name, diffs: 0) + merge_request = + merge_requests_table.create!(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: name, + title: name) + + diffs.times do + merge_request_diffs_table.create!(merge_request_id: merge_request.id) + end + + merge_request + end + + def diffs_for(merge_request) + merge_request_diffs_table.where(merge_request_id: merge_request.id) + end + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + diff_id = diffs_for(merge_request_migrated).minimum(:id) + merge_request_migrated.update!(latest_merge_request_diff_id: diff_id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id) + expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 3 + + migrate! + + expect(merge_requests_table.where(latest_merge_request_diff_id: nil).count).to eq 0 + end + end +end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 47342f98283..81e35e6c931 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -16,6 +16,23 @@ describe Blob do end end + describe '.lazy' do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit_by(oid: 'e63f41fe459e62e1228fcef60d7189127aeba95a') } + + it 'fetches all blobs when the first is accessed' do + changelog = described_class.lazy(project, commit.id, 'CHANGELOG') + contributing = described_class.lazy(project, commit.id, 'CONTRIBUTING.md') + + expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original + expect(Gitlab::Git::Blob).not_to receive(:find) + + # Access property so the values are loaded + changelog.id + contributing.id + end + end + describe '#data' do context 'using a binary blob' do it 'returns the data as-is' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2c9e7013b77..3a19a0753e2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -625,38 +625,29 @@ describe Ci::Pipeline, :mailer do shared_context 'with some outdated pipelines' do before do - create_pipeline(:canceled, 'ref', 'A') - create_pipeline(:success, 'ref', 'A') - create_pipeline(:failed, 'ref', 'B') - create_pipeline(:skipped, 'feature', 'C') + create_pipeline(:canceled, 'ref', 'A', project) + create_pipeline(:success, 'ref', 'A', project) + create_pipeline(:failed, 'ref', 'B', project) + create_pipeline(:skipped, 'feature', 'C', project) end - def create_pipeline(status, ref, sha) - create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + def create_pipeline(status, ref, sha, project) + create( + :ci_empty_pipeline, + status: status, + ref: ref, + sha: sha, + project: project + ) end end - describe '.latest' do + describe '.newest_first' do include_context 'with some outdated pipelines' - context 'when no ref is specified' do - let(:pipelines) { described_class.latest.all } - - it 'returns the latest pipeline for the same ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') - expect(pipelines.map(&:status)) - .to contain_exactly('success', 'failed', 'skipped') - end - end - - context 'when ref is specified' do - let(:pipelines) { described_class.latest('ref').all } - - it 'returns the latest pipeline for ref and different sha' do - expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') - expect(pipelines.map(&:status)) - .to contain_exactly('success', 'failed') - end + it 'returns the pipelines from new to old' do + expect(described_class.newest_first.pluck(:status)) + .to eq(%w[skipped failed success canceled]) end end @@ -664,20 +655,14 @@ describe Ci::Pipeline, :mailer do include_context 'with some outdated pipelines' context 'when no ref is specified' do - let(:latest_status) { described_class.latest_status } - - it 'returns the latest status for the same ref and different sha' do - expect(latest_status).to eq(described_class.latest.status) - expect(latest_status).to eq('failed') + it 'returns the status of the latest pipeline' do + expect(described_class.latest_status).to eq('skipped') end end context 'when ref is specified' do - let(:latest_status) { described_class.latest_status('ref') } - - it 'returns the latest status for ref and different sha' do - expect(latest_status).to eq(described_class.latest_status('ref')) - expect(latest_status).to eq('failed') + it 'returns the status of the latest pipeline for the given ref' do + expect(described_class.latest_status('ref')).to eq('failed') end end end @@ -686,7 +671,7 @@ describe Ci::Pipeline, :mailer do include_context 'with some outdated pipelines' let!(:latest_successful_pipeline) do - create_pipeline(:success, 'ref', 'D') + create_pipeline(:success, 'ref', 'D', project) end it 'returns the latest successful pipeline' do @@ -698,8 +683,13 @@ describe Ci::Pipeline, :mailer do describe '.latest_successful_for_refs' do include_context 'with some outdated pipelines' - let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') } - let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') } + let!(:latest_successful_pipeline1) do + create_pipeline(:success, 'ref1', 'D', project) + end + + let!(:latest_successful_pipeline2) do + create_pipeline(:success, 'ref2', 'D', project) + end it 'returns the latest successful pipeline for both refs' do refs = %w(ref1 ref2 ref3) @@ -708,6 +698,62 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_status_per_commit' do + let(:project) { create(:project) } + + before do + pairs = [ + %w[success ref1 123], + %w[manual master 123], + %w[failed ref 456] + ] + + pairs.each do |(status, ref, sha)| + create( + :ci_empty_pipeline, + status: status, + ref: ref, + sha: sha, + project: project + ) + end + end + + context 'without a ref' do + it 'returns a Hash containing the latest status per commit for all refs' do + expect(described_class.latest_status_per_commit(%w[123 456])) + .to eq({ '123' => 'manual', '456' => 'failed' }) + end + + it 'only includes the status of the given commit SHAs' do + expect(described_class.latest_status_per_commit(%w[123])) + .to eq({ '123' => 'manual' }) + end + + context 'when there are two pipelines for a ref and SHA' do + it 'returns the status of the latest pipeline' do + create( + :ci_empty_pipeline, + status: 'failed', + ref: 'master', + sha: '123', + project: project + ) + + expect(described_class.latest_status_per_commit(%w[123])) + .to eq({ '123' => 'failed' }) + end + end + end + + context 'with a ref' do + it 'only includes the pipelines for the given ref' do + expect(described_class.latest_status_per_commit(%w[123 456], 'master')) + .to eq({ '123' => 'manual' }) + end + end + end + describe '.internal_sources' do subject { described_class.internal_sources } @@ -1456,6 +1502,10 @@ describe Ci::Pipeline, :mailer do create(:ci_build, :success, :artifacts, pipeline: pipeline) end + it 'returns an Array' do + expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array) + end + it 'returns the latest builds' do expect(pipeline.latest_builds_with_artifacts).to eq([build]) end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb new file mode 100644 index 00000000000..066fe7d154e --- /dev/null +++ b/spec/models/commit_collection_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe CommitCollection do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + + describe '#each' do + it 'yields every commit' do + collection = described_class.new(project, [commit]) + + expect { |b| collection.each(&b) }.to yield_with_args(commit) + end + end + + describe '#with_pipeline_status' do + it 'sets the pipeline status for every commit so no additional queries are necessary' do + create( + :ci_empty_pipeline, + ref: 'master', + sha: commit.id, + status: 'success', + project: project + ) + + collection = described_class.new(project, [commit]) + collection.with_pipeline_status + + recorder = ActiveRecord::QueryRecorder.new do + expect(commit.status).to eq('success') + end + + expect(recorder.count).to be_zero + end + end + + describe '#respond_to_missing?' do + it 'returns true when the underlying Array responds to the message' do + collection = described_class.new(project, []) + + expect(collection.respond_to?(:last)).to eq(true) + end + + it 'returns false when the underlying Array does not respond to the message' do + collection = described_class.new(project, []) + + expect(collection.respond_to?(:foo)).to eq(false) + end + end + + describe '#method_missing' do + it 'delegates undefined methods to the underlying Array' do + collection = described_class.new(project, [commit]) + + expect(collection.length).to eq(1) + expect(collection.last).to eq(commit) + expect(collection).not_to be_empty + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index e3cfa149e3a..d18a5c9dfa6 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -351,12 +351,19 @@ eos end it 'gives compound status from latest pipelines if ref is nil' do - expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) - expect(commit.status(nil)).to eq('failed') + expect(commit.status(nil)).to eq(pipeline_from_fix.status) end end end + describe '#set_status_for_ref' do + it 'sets the status for a given reference' do + commit.set_status_for_ref('master', 'failed') + + expect(commit.status('master')).to eq('failed') + end + end + describe '#participants' do let(:user1) { build(:user) } let(:user2) { build(:user) } diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ba57301a3c9..4dfbb14952e 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -265,25 +265,44 @@ describe Issuable do end describe '#to_hook_data' do + let(:builder) { double } + context 'labels are updated' do let(:labels) { create_list(:label, 2) } before do issue.update(labels: [labels[1]]) + expect(Gitlab::HookData::IssuableBuilder) + .to receive(:new).with(issue).and_return(builder) end it 'delegates to Gitlab::HookData::IssuableBuilder#build' do - builder = double + expect(builder).to receive(:build).with( + user: user, + changes: hash_including( + 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]] + )) + issue.to_hook_data(user, old_labels: [labels[0]]) + end + end + + context 'total_time_spent is updated' do + before do + issue.spend_time(duration: 2, user: user, spent_at: Time.now) + issue.save expect(Gitlab::HookData::IssuableBuilder) .to receive(:new).with(issue).and_return(builder) + end + + it 'delegates to Gitlab::HookData::IssuableBuilder#build' do expect(builder).to receive(:build).with( user: user, changes: hash_including( - 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]] + 'total_time_spent' => [1, 2] )) - issue.to_hook_data(user, old_labels: [labels[0]]) + issue.to_hook_data(user, old_total_time_spent: 1) end end @@ -292,13 +311,11 @@ describe Issuable do before do issue.assignees << user << user2 + expect(Gitlab::HookData::IssuableBuilder) + .to receive(:new).with(issue).and_return(builder) end it 'delegates to Gitlab::HookData::IssuableBuilder#build' do - builder = double - - expect(Gitlab::HookData::IssuableBuilder) - .to receive(:new).with(issue).and_return(builder) expect(builder).to receive(:build).with( user: user, changes: hash_including( @@ -316,13 +333,11 @@ describe Issuable do before do merge_request.update(assignee: user) merge_request.update(assignee: user2) + expect(Gitlab::HookData::IssuableBuilder) + .to receive(:new).with(merge_request).and_return(builder) end it 'delegates to Gitlab::HookData::IssuableBuilder#build' do - builder = double - - expect(Gitlab::HookData::IssuableBuilder) - .to receive(:new).with(merge_request).and_return(builder) expect(builder).to receive(:build).with( user: user, changes: hash_including( diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb index b26de3f3b97..c90b32c5d77 100644 --- a/spec/models/diff_viewer/base_spec.rb +++ b/spec/models/diff_viewer/base_spec.rb @@ -32,10 +32,8 @@ describe DiffViewer::Base do end context 'when the binaryness does not match' do - before do - allow(diff_file.old_blob).to receive(:binary?).and_return(false) - allow(diff_file.new_blob).to receive(:binary?).and_return(false) - end + let(:commit) { project.commit_by(oid: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('Gemfile.zip') } it 'returns false' do expect(viewer_class.can_render?(diff_file)).to be_falsey @@ -60,8 +58,7 @@ describe DiffViewer::Base do context 'when the binaryness does not match' do before do - allow(diff_file.old_blob).to receive(:binary?).and_return(true) - allow(diff_file.new_blob).to receive(:binary?).and_return(true) + allow_any_instance_of(Blob).to receive(:binary?).and_return(true) end it 'returns false' do @@ -77,12 +74,12 @@ describe DiffViewer::Base do end context 'when the file was renamed and only the old blob is supported' do - let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:commit) { project.commit_by(oid: '2f63565e7aac07bcdadb654e253078b727143ec4') } let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } before do allow(diff_file).to receive(:renamed_file?).and_return(true) - allow(diff_file.new_blob).to receive(:extension).and_return('jpeg') + viewer_class.extensions = %w(notjpg) end it 'returns false' do @@ -94,8 +91,7 @@ describe DiffViewer::Base do describe '#collapsed?' do context 'when the combined blob size is larger than the collapse limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes) + allow(diff_file).to receive(:raw_size).and_return(1025.kilobytes) end it 'returns true' do @@ -113,8 +109,7 @@ describe DiffViewer::Base do describe '#too_large?' do context 'when the combined blob size is larger than the size limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + allow(diff_file).to receive(:raw_size).and_return(6.megabytes) end it 'returns true' do @@ -132,8 +127,7 @@ describe DiffViewer::Base do describe '#render_error' do context 'when the combined blob size is larger than the size limit' do before do - allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) - allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + allow(diff_file).to receive(:raw_size).and_return(6.megabytes) end it 'returns :too_large' do diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb index 92e613f92de..98a8f6d4cc9 100644 --- a/spec/models/diff_viewer/server_side_spec.rb +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe DiffViewer::ServerSide do - let(:project) { create(:project, :repository) } - let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } - let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + set(:project) { create(:project, :repository) } + let(:commit) { project.commit_by(oid: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let!(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } let(:viewer_class) do Class.new(DiffViewer::Base) do @@ -15,8 +15,7 @@ describe DiffViewer::ServerSide do describe '#prepare!' do it 'loads all diff file data' do - expect(diff_file.old_blob).to receive(:load_all_data!) - expect(diff_file.new_blob).to receive(:load_all_data!) + expect(Blob).to receive(:lazy).at_least(:twice) subject.prepare! end diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb index 532ca1fca8c..25bf596fddc 100644 --- a/spec/models/fork_network_member_spec.rb +++ b/spec/models/fork_network_member_spec.rb @@ -5,4 +5,22 @@ describe ForkNetworkMember do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:fork_network) } end + + describe 'destroying a ForkNetworkMember' do + let(:fork_network_member) { create(:fork_network_member) } + let(:fork_network) { fork_network_member.fork_network } + + it 'removes the fork network if it was the last member' do + fork_network.fork_network_members.destroy_all + + expect(ForkNetwork.count).to eq(0) + end + + it 'does not destroy the fork network if there are members left' do + fork_network_member.destroy! + + # The root of the fork network is left + expect(ForkNetwork.count).to eq(1) + end + end end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 3ed048744de..a45a6088831 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -33,5 +33,15 @@ describe Identity do expect(identity).to eq(ldap_identity) end end + + context 'any other provider' do + let!(:test_entity) { create(:identity, provider: 'test_provider', extern_uid: 'test_uid') } + + it 'the extern_uid lookup is case insensitive' do + identity = described_class.with_extern_uid('test_provider', 'TEST_UID').first + + expect(identity).to eq(test_entity) + end + end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 81c2057e175..4cd9e3f4f1d 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -166,4 +166,27 @@ describe Key, :mailer do expect(key.public_key.key_text).to eq(valid_key) end end + + describe '#refresh_user_cache', :use_clean_rails_memory_store_caching do + context 'when the key belongs to a user' do + it 'refreshes the keys count cache for the user' do + expect_any_instance_of(Users::KeysCountService) + .to receive(:refresh_cache) + .and_call_original + + key = create(:personal_key) + + expect(Users::KeysCountService.new(key.user).count).to eq(1) + end + end + + context 'when the key does not belong to a user' do + it 'does nothing' do + expect_any_instance_of(Users::KeysCountService) + .not_to receive(:refresh_cache) + + create(:key) + end + end + end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 13e37fffa4e..47f4a792e5c 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -11,7 +11,7 @@ describe Milestone do milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday) expect(milestone).not_to be_valid - expect(milestone.errors[:start_date]).to include("Can't be greater than due date") + expect(milestone.errors[:due_date]).to include("must be greater than start date") end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 1ecb50586c7..6e7e8c4c570 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -231,6 +231,37 @@ describe Note do end end + describe '#cross_reference?' do + it 'falsey for user-generated notes' do + note = create(:note, system: false) + + expect(note.cross_reference?).to be_falsy + end + + context 'when the note might contain cross references' do + SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.each do |type| + let(:note) { create(:note, :system) } + let!(:metadata) { create(:system_note_metadata, note: note, action: type) } + + it 'delegates to the cross-reference regex' do + expect(note).to receive(:matches_cross_reference_regex?).and_return(false) + + note.cross_reference? + end + end + end + + context 'when the note cannot contain cross references' do + let(:commit_note) { build(:note, note: 'mentioned in 1312312313 something else.', system: true) } + let(:label_note) { build(:note, note: 'added ~2323232323', system: true) } + + it 'scan for a `mentioned in` prefix' do + expect(commit_note.cross_reference?).to be_truthy + expect(label_note.cross_reference?).to be_falsy + end + end + end + describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ') diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 5e8e880985e..fabcb142858 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -46,6 +46,7 @@ describe FlowdockService do @sample_data[:commits].each do |commit| # One request to Flowdock per new commit next if commit[:id] == @sample_data[:before] + expect(WebMock).to have_requested(:post, @api_url).with( body: /#{commit[:id]}.*#{project.path}/ ).once diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 3d46434fc27..929086305ba 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -10,6 +10,10 @@ describe ProjectWiki do subject { project_wiki } + it { is_expected.to delegate_method(:empty?).to :pages } + it { is_expected.to delegate_method(:repository_storage_path).to :project } + it { is_expected.to delegate_method(:hashed_storage?).to :project } + describe "#path_with_namespace" do it "returns the project path with namespace with the .wiki extension" do expect(subject.path_with_namespace).to eq(project.full_path + '.wiki') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 88732962071..86647ddf6ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -828,7 +828,7 @@ describe User do end end - describe '#require_ssh_key?' do + describe '#require_ssh_key?', :use_clean_rails_memory_store_caching do protocol_and_expectation = { 'http' => false, 'ssh' => true, @@ -843,6 +843,12 @@ describe User do expect(user.require_ssh_key?).to eq(expected) end end + + it 'returns false when the user has 1 or more SSH keys' do + key = create(:personal_key) + + expect(key.user.require_ssh_key?).to eq(false) + end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index a7227b38850..ea75434e399 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -373,7 +373,7 @@ describe WikiPage do end it 'returns commit sha' do - expect(@page.last_commit_sha).to eq @page.commit.sha + expect(@page.last_commit_sha).to eq @page.last_version.sha end it 'is changed after page updated' do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 6c0996c543d..0462f494e15 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -11,7 +11,6 @@ describe API::Helpers do let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } - let(:params) { {} } let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) } let(:env) do { @@ -19,10 +18,13 @@ describe API::Helpers do 'rack.session' => { _csrf_token: csrf_token }, - 'REQUEST_METHOD' => 'GET' + 'REQUEST_METHOD' => 'GET', + 'CONTENT_TYPE' => 'text/plain;charset=utf-8' } end let(:header) { } + let(:request) { Grape::Request.new(env)} + let(:params) { request.params } before do allow_any_instance_of(self.class).to receive(:options).and_return({}) @@ -37,6 +39,10 @@ describe API::Helpers do raise Exception.new("#{status} - #{message}") end + def set_param(key, value) + request.update_param(key, value) + end + describe ".current_user" do subject { current_user } @@ -132,13 +138,13 @@ describe API::Helpers do let(:personal_access_token) { create(:personal_access_token, user: user) } it "returns a 401 response for an invalid token" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = 'invalid token' expect { current_user }.to raise_error /401/ end it "returns a 403 response for a user without access" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) expect { current_user }.to raise_error /403/ @@ -146,35 +152,35 @@ describe API::Helpers do it 'returns a 403 response for a user who is blocked' do user.block! - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect { current_user }.to raise_error /403/ end it "sets current_user" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) end it "does not allow tokens without the appropriate scope" do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError end it 'does not allow revoked tokens' do personal_access_token.revoke! - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::RevokedError + expect { current_user }.to raise_error Gitlab::Auth::RevokedError end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error API::APIGuard::ExpiredError + expect { current_user }.to raise_error Gitlab::Auth::ExpiredError end end end @@ -350,7 +356,7 @@ describe API::Helpers do context 'when using param' do context 'when providing username' do before do - params[API::Helpers::SUDO_PARAM] = user.username + set_param(API::Helpers::SUDO_PARAM, user.username) end it_behaves_like 'successful sudo' @@ -358,7 +364,7 @@ describe API::Helpers do context 'when providing user ID' do before do - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it_behaves_like 'successful sudo' @@ -368,7 +374,7 @@ describe API::Helpers do context 'when user does not exist' do before do - params[API::Helpers::SUDO_PARAM] = 'nonexistent' + set_param(API::Helpers::SUDO_PARAM, 'nonexistent') end it 'raises an error' do @@ -382,11 +388,11 @@ describe API::Helpers do token.scopes = %w[api] token.save! - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it 'raises an error' do - expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + expect { current_user }.to raise_error Gitlab::Auth::InsufficientScopeError end end end @@ -396,7 +402,7 @@ describe API::Helpers do token.user = user token.save! - params[API::Helpers::SUDO_PARAM] = user.id.to_s + set_param(API::Helpers::SUDO_PARAM, user.id.to_s) end it 'raises an error' do @@ -420,7 +426,7 @@ describe API::Helpers do context 'passed as param' do before do - params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token + set_param(Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_PARAM, token.token) end it_behaves_like 'sudo' @@ -428,7 +434,7 @@ describe API::Helpers do context 'passed as header' do before do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = token.token end it_behaves_like 'sudo' diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index d919899282d..34ecdd1e164 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -203,18 +203,44 @@ describe API::Internal do end context 'with env passed as a JSON' do - it 'sets env in RequestStore' do - expect(Gitlab::Git::Env).to receive(:set).with({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - }) + context 'when relative path envs are not set' do + it 'sets env in RequestStore' do + expect(Gitlab::Git::Env).to receive(:set).with({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }) + + push(key, project.wiki, env: { + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' + }.to_json) - push(key, project.wiki, env: { - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' - }.to_json) + expect(response).to have_gitlab_http_status(200) + end + end - expect(response).to have_gitlab_http_status(200) + context 'when relative path envs are set' do + it 'sets env in RequestStore' do + obj_dir_relative = './objects' + alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2'] + repo_path = project.wiki.repository.path_to_repo + + expect(Gitlab::Git::Env).to receive(:set).with({ + 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative), + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) }, + 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative, + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative + }) + + push(key, project.wiki, env: { + GIT_OBJECT_DIRECTORY: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar', + GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative + }.to_json) + + expect(response).to have_gitlab_http_status(200) + end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 50f6c8b7d64..a41345da05b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -437,6 +437,7 @@ describe API::Projects do project.each_pair do |k, v| next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + expect(json_response[k.to_s]).to eq(v) end @@ -643,6 +644,7 @@ describe API::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 2aeae6f9ec7..2428e63e149 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -510,6 +510,14 @@ describe API::Users do expect(user.reload.notification_email).to eq('new@email.com') end + it 'skips reconfirmation when requested' do + put api("/users/#{user.id}", admin), { skip_reconfirmation: true } + + user.reload + + expect(user.confirmed_at).to be_present + end + it 'updates user with his own username' do put api("/users/#{user.id}", admin), username: user.username diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index f62ad747c73..27288b98d1c 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -404,6 +404,7 @@ describe API::V3::Projects do project.each_pair do |k, v| next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + expect(json_response[k.to_s]).to eq(v) end @@ -547,6 +548,7 @@ describe API::V3::Projects do expect(response).to have_gitlab_http_status(201) project.each_pair do |k, v| next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 0b1f8ce6f6d..1a5ad9b04e4 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -107,6 +107,15 @@ describe 'OpenID Connect requests' do end end + # These 2 calls shouldn't actually throw, they should be handled as an + # unauthorized request, so we should be able to check the response. + # + # This was not possible due to an issue with Warden: + # https://github.com/hassox/warden/pull/162 + # + # When the patch gets merged and we update Warden, these specs will need to + # updated to check the response instead of a raised exception. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/40218 context 'when user is blocked' do it 'returns authentication error' do access_grant @@ -114,7 +123,7 @@ describe 'OpenID Connect requests' do expect do request_access_token - end.to throw_symbol :warden + end.to raise_error UncaughtThrowError end end @@ -125,7 +134,7 @@ describe 'OpenID Connect requests' do expect do request_access_token - end.to throw_symbol :warden + end.to raise_error UncaughtThrowError end end end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb new file mode 100644 index 00000000000..0fec14d0cce --- /dev/null +++ b/spec/requests/rack_attack_global_spec.rb @@ -0,0 +1,362 @@ +require 'spec_helper' + +describe 'Rack Attack global throttles' do + let(:settings) { Gitlab::CurrentSettings.current_application_settings } + + # Start with really high limits and override them with low limits to ensure + # the right settings are being exercised + let(:settings_to_set) do + { + throttle_unauthenticated_requests_per_period: 100, + throttle_unauthenticated_period_in_seconds: 1, + throttle_authenticated_api_requests_per_period: 100, + throttle_authenticated_api_period_in_seconds: 1, + throttle_authenticated_web_requests_per_period: 100, + throttle_authenticated_web_period_in_seconds: 1 + } + end + + let(:requests_per_period) { 1 } + let(:period_in_seconds) { 10000 } + let(:period) { period_in_seconds.seconds } + + let(:url_that_does_not_require_authentication) { '/users/sign_in' } + let(:url_that_requires_authentication) { '/dashboard/snippets' } + let(:api_partial_url) { '/todos' } + + around do |example| + # Instead of test environment's :null_store so the throttles can increment + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + + # Make time-dependent tests deterministic + Timecop.freeze { example.run } + + Rack::Attack.cache.store = Rails.cache + end + + # Requires let variables: + # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web") + # * get_args + # * other_user_get_args + shared_examples_for 'rate-limited token-authenticated requests' do + before do + # Set low limits + settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period + settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get(*get_args) } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_rejection { get(*get_args) } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + get(*other_user_get_args) + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get(*get_args) + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get(*get_args) } + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get(*get_args) + expect(response).to have_http_status 200 + end + end + end + end + + describe 'unauthenticated requests' do + before do + # Set low limits + settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period + settings_to_set[:throttle_unauthenticated_period_in_seconds] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:throttle_unauthenticated_enabled] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get url_that_does_not_require_authentication } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_does_not_require_authentication } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_does_not_require_authentication } + end + end + + it 'counts requests from different IPs separately' do + requests_per_period.times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + # would be over limit for the same IP + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:throttle_unauthenticated_enabled] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_does_not_require_authentication + expect(response).to have_http_status 200 + end + end + end + end + + describe 'API requests authenticated with personal access token', :api do + let(:user) { create(:user) } + let(:token) { create(:personal_access_token, user: user) } + let(:other_user) { create(:user) } + let(:other_user_token) { create(:personal_access_token, user: other_user) } + let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + + context 'with the token in the query string' do + let(:get_args) { [api(api_partial_url, personal_access_token: token)] } + let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + + context 'with the token in the headers' do + let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) } + let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe 'API requests authenticated with OAuth token', :api do + let(:user) { create(:user) } + let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") } + let(:other_user) { create(:user) } + let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) } + let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") } + let(:throttle_setting_prefix) { 'throttle_authenticated_api' } + + context 'with the token in the query string' do + let(:get_args) { [api(api_partial_url, oauth_access_token: token)] } + let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + + context 'with the token in the headers' do + let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) } + let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe '"web" (non-API) requests authenticated with RSS token' do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:throttle_setting_prefix) { 'throttle_authenticated_web' } + + context 'with the token in the query string' do + let(:get_args) { [rss_url(user), nil] } + let(:other_user_get_args) { [rss_url(other_user), nil] } + + it_behaves_like 'rate-limited token-authenticated requests' + end + end + + describe 'web requests authenticated with regular login' do + let(:user) { create(:user) } + + before do + login_as(user) + + # Set low limits + settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period + settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds + end + + context 'when the throttle is enabled' do + before do + settings_to_set[:throttle_authenticated_web_enabled] = true + stub_application_setting(settings_to_set) + end + + it 'rejects requests over the rate limit' do + # At first, allow requests under the rate limit. + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # the last straw + expect_rejection { get url_that_requires_authentication } + end + + it 'allows requests after throttling and then waiting for the next period' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + + Timecop.travel(period.from_now) do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_rejection { get url_that_requires_authentication } + end + end + + it 'counts requests from different users separately, even from the same IP' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + # would be over the limit if this wasn't a different user + login_as(create(:user)) + + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + it 'counts all requests from the same user, even via different IPs' do + requests_per_period.times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + + expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4') + + expect_rejection { get url_that_requires_authentication } + end + end + + context 'when the throttle is disabled' do + before do + settings_to_set[:throttle_authenticated_web_enabled] = false + stub_application_setting(settings_to_set) + end + + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_that_requires_authentication + expect(response).to have_http_status 200 + end + end + end + end + + def api_get_args_with_token_headers(partial_url, token_headers) + ["/api/#{API::API.version}#{partial_url}", nil, token_headers] + end + + def rss_url(user) + "/dashboard/projects.atom?rss_token=#{user.rss_token}" + end + + def private_token_headers(user) + { 'HTTP_PRIVATE_TOKEN' => user.private_token } + end + + def personal_access_token_headers(personal_access_token) + { 'HTTP_PRIVATE_TOKEN' => personal_access_token.token } + end + + def oauth_token_headers(oauth_access_token) + { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" } + end + + def expect_rejection(&block) + yield + + expect(response).to have_http_status(429) + end +end diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb index 7a4c8304e62..71788028cbf 100644 --- a/spec/routing/group_routing_spec.rb +++ b/spec/routing/group_routing_spec.rb @@ -39,13 +39,19 @@ describe "Groups", "routing" do describe 'legacy redirection' do describe 'labels' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels", "/groups/complex.group-namegit/-/labels" do let(:resource) { create(:group, parent: group, path: 'labels') } end + + context 'when requesting JSON' do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/labels.json", "/groups/complex.group-namegit/-/labels.json" do + let(:resource) { create(:group, parent: group, path: 'labels') } + end + end end describe 'group_members' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/group_members", "/groups/complex.group-namegit/-/group_members" do let(:resource) { create(:group, parent: group, path: 'group_members') } end end @@ -60,7 +66,7 @@ describe "Groups", "routing" do end describe 'milestones' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones", "/groups/complex.group-namegit/-/milestones" do let(:resource) { create(:group, parent: group, path: 'milestones') } end @@ -76,18 +82,18 @@ describe "Groups", "routing" do end context 'with a query string' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones/?hello=world" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?hello=world", "/groups/complex.group-namegit/-/milestones?hello=world" do let(:resource) { create(:group, parent: group, path: 'milestones') } end - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones/?milestones=/milestones" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/milestones?milestones=/milestones", "/groups/complex.group-namegit/-/milestones?milestones=/milestones" do let(:resource) { create(:group, parent: group, path: 'milestones') } end end end describe 'edit' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/edit", "/groups/complex.group-namegit/-/edit" do let(:resource) do pending('still rejected because of the wildcard reserved word') create(:group, parent: group, path: 'edit') @@ -96,29 +102,29 @@ describe "Groups", "routing" do end describe 'issues' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/issues", "/groups/complex.group-namegit/-/issues" do let(:resource) { create(:group, parent: group, path: 'issues') } end end describe 'merge_requests' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/merge_requests", "/groups/complex.group-namegit/-/merge_requests" do let(:resource) { create(:group, parent: group, path: 'merge_requests') } end end describe 'projects' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/projects", "/groups/complex.group-namegit/-/projects" do let(:resource) { create(:group, parent: group, path: 'projects') } end end describe 'activity' do - it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity/" do + it_behaves_like 'redirecting a legacy path', "/groups/complex.group-namegit/activity", "/groups/complex.group-namegit/-/activity" do let(:resource) { create(:group, parent: group, path: 'activity') } end - it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity/" do + it_behaves_like 'redirecting a legacy path', "/groups/activity/activity", "/groups/activity/-/activity" do let!(:parent) { create(:group, path: 'activity') } let(:resource) { create(:group, parent: parent, path: 'activity') } end diff --git a/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb new file mode 100644 index 00000000000..8899dc85384 --- /dev/null +++ b/spec/rubocop/cop/line_break_after_guard_clauses_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/line_break_after_guard_clauses' + +describe RuboCop::Cop::LineBreakAfterGuardClauses do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples 'examples with guard clause' do |title| + %w[if unless].each do |conditional| + it "flags violation for #{title} #{conditional} without line breaks" do + source = <<~RUBY + #{title} #{conditional} condition + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(1) + expect(cop.highlights).to eq(["#{title} #{conditional} condition"]) + expect(offense.message).to eq('Add a line break after guard clauses') + end + + it "doesn't flag violation for #{title} #{conditional} with line break" do + source = <<~RUBY + #{title} #{conditional} condition + + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} on multiple lines without line break" do + source = <<~RUBY + #{conditional} condition + #{title} + end + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by end keyword" do + source = <<~RUBY + def test + #{title} #{conditional} condition + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by elsif keyword" do + source = <<~RUBY + if model + #{title} #{conditional} condition + elsif + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by else keyword" do + source = <<~RUBY + if model + #{title} #{conditional} condition + else + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by when keyword" do + source = <<~RUBY + case model + when condition_a + #{title} #{conditional} condition + when condition_b + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by rescue keyword" do + source = <<~RUBY + begin + #{title} #{conditional} condition + rescue StandardError + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by ensure keyword" do + source = <<~RUBY + def foo + #{title} #{conditional} condition + ensure + do_something + end + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} #{conditional} without line breaks when followed by another guard clause" do + source = <<~RUBY + #{title} #{conditional} condition + #{title} #{conditional} condition + + do_stuff + RUBY + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + + it "autocorrects #{title} #{conditional} guard clauses without line break" do + source = <<~RUBY + #{title} #{conditional} condition + do_stuff + RUBY + autocorrected = autocorrect_source(cop, source) + + expected_source = <<~RUBY + #{title} #{conditional} condition + + do_stuff + RUBY + expect(autocorrected).to eql(expected_source) + end + end + end + + %w[return fail raise next break throw].each do |example| + it_behaves_like 'examples with guard clause', example + end +end diff --git a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb deleted file mode 100644 index 07cb3fc4a2e..00000000000 --- a/spec/rubocop/cop/migration/add_column_with_default_to_large_table_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -require 'rubocop' -require 'rubocop/rspec/support' - -require_relative '../../../../rubocop/cop/migration/add_column_with_default_to_large_table' - -describe RuboCop::Cop::Migration::AddColumnWithDefaultToLargeTable do - include CopHelper - - subject(:cop) { described_class.new } - - context 'in migration' do - before do - allow(cop).to receive(:in_migration?).and_return(true) - end - - described_class::LARGE_TABLES.each do |table| - it "registers an offense for the #{table} table" do - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") - - aggregate_failures do - expect(cop.offenses.size).to eq(1) - expect(cop.offenses.map(&:line)).to eq([1]) - end - end - end - - it 'registers no offense for non-blacklisted tables' do - inspect_source(cop, "add_column_with_default :table, :column, default: true") - - expect(cop.offenses).to be_empty - end - end - - context 'outside of migration' do - it 'registers no offense' do - table = described_class::LARGE_TABLES.sample - inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") - - expect(cop.offenses).to be_empty - end - end -end diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb new file mode 100644 index 00000000000..17b19e139e4 --- /dev/null +++ b/spec/rubocop/cop/migration/update_large_table_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/update_large_table' + +describe RuboCop::Cop::Migration::UpdateLargeTable do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + shared_examples 'large tables' do |update_method| + described_class::LARGE_TABLES.each do |table| + it "registers an offense for the #{table} table" do + inspect_source(cop, "#{update_method} :#{table}, :column, default: true") + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + end + + context 'for the add_column_with_default method' do + include_examples 'large tables', 'add_column_with_default' + end + + context 'for the update_column_in_batches method' do + include_examples 'large tables', 'update_column_in_batches' + end + + it 'registers no offense for non-blacklisted tables' do + inspect_source(cop, "add_column_with_default :table, :column, default: true") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for non-blacklisted methods' do + table = described_class::LARGE_TABLES.sample + + inspect_source(cop, "some_other_method :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + end + + context 'outside of migration' do + let(:table) { described_class::LARGE_TABLES.sample } + + it 'registers no offense for add_column_with_default' do + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + + it 'registers no offense for update_column_in_batches' do + inspect_source(cop, "add_column_with_default :#{table}, :column, default: true") + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb new file mode 100644 index 00000000000..5ec8ed0976d --- /dev/null +++ b/spec/services/base_count_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe BaseCountService, :use_clean_rails_memory_store_caching do + let(:service) { described_class.new } + + describe '#relation_for_count' do + it 'raises NotImplementedError' do + expect { service.relation_for_count }.to raise_error(NotImplementedError) + end + end + + describe '#count' do + it 'returns the number of values' do + expect(service) + .to receive(:cache_key) + .and_return('foo') + + expect(service) + .to receive(:uncached_count) + .and_return(5) + + expect(service.count).to eq(5) + end + end + + describe '#uncached_count' do + it 'returns the uncached number of values' do + expect(service) + .to receive(:relation_for_count) + .and_return(double(:relation, count: 5)) + + expect(service.uncached_count).to eq(5) + end + end + + describe '#refresh_cache' do + it 'refreshes the cache' do + allow(service) + .to receive(:cache_key) + .and_return('foo') + + allow(service) + .to receive(:uncached_count) + .and_return(4) + + service.refresh_cache + + expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to eq(4) + end + end + + describe '#delete_cache' do + it 'deletes the cache' do + allow(service) + .to receive(:cache_key) + .and_return('foo') + + allow(service) + .to receive(:uncached_count) + .and_return(4) + + service.refresh_cache + service.delete_cache + + expect(Rails.cache.fetch(service.cache_key, raw: service.raw?)).to be_nil + end + end + + describe '#raw?' do + it 'returns false' do + expect(service.raw?).to eq(false) + end + end + + describe '#cache_key' do + it 'raises NotImplementedError' do + expect { service.cache_key }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 98409be4236..5ce6ca70c83 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -80,7 +80,7 @@ describe MergeRequests::UpdateService, :mailer do it 'executes hooks with update action' do expect(service) .to have_received(:execute_hooks) - .with(@merge_request, 'update', old_labels: [], old_assignees: [user3]) + .with(@merge_request, 'update', old_labels: [], old_assignees: [user3], old_total_time_spent: 0) end it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb index 16e288b3148..af35e17bfa7 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -5,7 +5,7 @@ describe Milestones::DestroyService do let(:project) { create(:project) } let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } let!(:issue) { create(:issue, project: project, milestone: milestone) } - let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } before do project.team << [user, :master] diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb index 9f2df6d6d19..a0a2843b676 100644 --- a/spec/services/milestones/promote_service_spec.rb +++ b/spec/services/milestones/promote_service_spec.rb @@ -25,6 +25,18 @@ describe Milestones::PromoteService do expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) end + + it 'does not promote milestone and update issuables if promoted milestone is not valid' do + issue = create(:issue, milestone: milestone, project: project) + merge_request = create(:merge_request, milestone: milestone, source_project: project) + allow_any_instance_of(Milestone).to receive(:valid?).and_return(false) + + expect { service.execute(milestone) }.to raise_error(described_class::PromoteMilestoneError) + + expect(milestone.reload).to be_persisted + expect(issue.reload.milestone).to eq(milestone) + expect(merge_request.reload.milestone).to eq(milestone) + end end context 'without duplicated milestone titles across projects' do @@ -34,6 +46,16 @@ describe Milestones::PromoteService do expect(promoted_milestone).to be_group_milestone end + it 'does not update issuables without milestone with the new promoted milestone' do + issue_without_milestone = create(:issue, project: project, milestone: nil) + merge_request_without_milestone = create(:merge_request, milestone: nil, source_project: project) + + service.execute(milestone) + + expect(issue_without_milestone.reload.milestone).to be_nil + expect(merge_request_without_milestone.reload.milestone).to be_nil + end + it 'sets issuables with new promoted milestone' do issue = create(:issue, milestone: milestone, project: project) merge_request = create(:merge_request, milestone: milestone, source_project: project) @@ -59,6 +81,20 @@ describe Milestones::PromoteService do expect(Milestone.exists?(milestone_2.id)).to be_falsy end + it 'does not update issuables without milestone with the new promoted milestone' do + issue_without_milestone_1 = create(:issue, project: project, milestone: nil) + issue_without_milestone_2 = create(:issue, project: project_2, milestone: nil) + merge_request_without_milestone_1 = create(:merge_request, milestone: nil, source_project: project) + merge_request_without_milestone_2 = create(:merge_request, milestone: nil, source_project: project_2) + + service.execute(milestone) + + expect(issue_without_milestone_1.reload.milestone).to be_nil + expect(issue_without_milestone_2.reload.milestone).to be_nil + expect(merge_request_without_milestone_1.reload.milestone).to be_nil + expect(merge_request_without_milestone_2.reload.milestone).to be_nil + end + it 'sets all issuables with new promoted milestone' do issue = create(:issue, milestone: milestone, project: project) issue_2 = create(:issue, milestone: milestone_2, project: project_2) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index b13e12e7c94..db5de572b6d 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -280,6 +280,7 @@ describe NotificationService, :mailer do next if member.id == @u_disabled.id # Author should not be notified next if member.id == note.author.id + should_email(member) end @@ -327,6 +328,7 @@ describe NotificationService, :mailer do next if member.id == @u_disabled.id # Author should not be notified next if member.id == note.author.id + should_email(member) end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 2459f371a91..2b1337bee7e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -42,6 +42,18 @@ describe Projects::TransferService do expect(service).to receive(:execute_system_hooks) end end + + it 'disk path has moved' do + old_path = project.repository.disk_path + old_full_path = project.repository.full_path + + transfer_project(project, user, group) + + expect(project.repository.disk_path).not_to eq(old_path) + expect(project.repository.full_path).not_to eq(old_full_path) + expect(project.disk_path).not_to eq(old_path) + expect(project.disk_path).to start_with(group.path) + end end context 'when transfer fails' do @@ -188,6 +200,26 @@ describe Projects::TransferService do end end + context 'when hashed storage in use' do + let(:hashed_project) { create(:project, :repository, :hashed, namespace: user.namespace) } + + before do + group.add_owner(user) + end + + it 'does not move the directory' do + old_path = hashed_project.repository.disk_path + old_full_path = hashed_project.repository.full_path + + transfer_project(hashed_project, user, group) + project.reload + + expect(hashed_project.repository.disk_path).to eq(old_path) + expect(hashed_project.repository.full_path).to eq(old_full_path) + expect(hashed_project.disk_path).to eq(old_path) + end + end + describe 'refreshing project authorizations' do let(:group) { create(:group) } let(:owner) { project.namespace.owner } diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb new file mode 100644 index 00000000000..a188cf86772 --- /dev/null +++ b/spec/services/users/keys_count_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Users::KeysCountService, :use_clean_rails_memory_store_caching do + let(:user) { create(:user) } + let(:service) { described_class.new(user) } + + describe '#count' do + before do + create(:personal_key, user: user) + end + + it 'returns the number of SSH keys as an Integer' do + expect(service.count).to eq(1) + end + + it 'caches the number of keys in Redis' do + service.delete_cache + + recorder = ActiveRecord::QueryRecorder.new do + 2.times { service.count } + end + + expect(recorder.count).to eq(1) + end + end + + describe '#refresh_cache' do + it 'refreshes the Redis cache' do + Rails.cache.write(service.cache_key, 10) + service.refresh_cache + + expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_zero + end + end + + describe '#delete_cache' do + it 'removes the cache' do + service.count + service.delete_cache + + expect(Rails.cache.fetch(service.cache_key, raw: true)).to be_nil + end + end + + describe '#uncached_count' do + it 'returns the number of SSH keys' do + expect(service.uncached_count).to be_zero + end + + it 'does not cache the number of keys' do + recorder = ActiveRecord::QueryRecorder.new do + 2.times { service.uncached_count } + end + + expect(recorder.count).to be > 0 + end + end + + describe '#cache_key' do + it 'returns the cache key' do + expect(service.cache_key).to eq("users/key-count-service/#{user.id}") + end + end +end diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index 5515c355cea..128aaaf25fe 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -1,6 +1,7 @@ module FixtureHelpers def fixture_file(filename) return '' if filename.blank? + File.read(expand_fixture_path(filename)) end diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index ef3c8e7087f..4ee33f9725b 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -33,6 +33,7 @@ end def capture!(cmd, dir) output = IO.popen(cmd, 'r', chdir: dir) { |io| io.read } raise "command failed with #{$?}: #{cmd.join(' ')}" unless $?.success? + output.chomp end diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index 1512b3e0620..c7e8a39a617 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -4,6 +4,7 @@ RSpec.configure do |config| allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) else next if example.metadata[:skip_gitaly_mock] + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) end end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 5dd8fe8eaa5..a51374e2645 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -47,7 +47,7 @@ describe 'gitlab:gitaly namespace rake task' do stub_env('CI', false) FileUtils.mkdir_p(clone_path) expect(Dir).to receive(:chdir).with(clone_path).and_call_original - allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path') + allow(Rails.env).to receive(:test?).and_return(false) end context 'gmake is available' do @@ -57,7 +57,7 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls gmake in the gitaly directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -70,18 +70,20 @@ describe 'gitlab:gitaly namespace rake task' do end it 'calls make in the gitaly directory' do - expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end - context 'when Rails.env is not "test"' do + context 'when Rails.env is test' do + let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] } + before do - allow(Rails.env).to receive(:test?).and_return(false) + allow(Rails.env).to receive(:test?).and_return(true) end - it 'calls make in the gitaly directory without BUNDLE_PATH' do - expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + it 'calls make in the gitaly directory with --no-deployment flag for bundle' do + expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb index 41de94d35c2..79a566975df 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/unicorn/unicorn_spec.rb @@ -71,6 +71,7 @@ describe 'Unicorn' do timeout = 5 * 60 timeout.times do return if File.exist?(ready_file) + pid = Process.waitpid(master_pid, Process::WNOHANG) raise "unicorn failed to boot: #{$?}" unless pid.nil? diff --git a/yarn.lock b/yarn.lock index bf92370d44f..a73aebbf180 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@gitlab-org/gitlab-svgs@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.0.2.tgz#e4d29058e2bb438ba71ac525c6397ef15ae2877b" + abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -260,6 +264,12 @@ aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios-mock-adapter@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d" + dependencies: + deep-equal "^1.0.1" + axios@^0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" @@ -2720,10 +2730,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git": - version "1.0.4" - resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#0442503549e6d74a4e22e1641e1d2ab0ae09884b" - glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" |