diff options
605 files changed, 11403 insertions, 3747 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6a5050b553f..07969475503 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 @@ -416,12 +416,8 @@ ee_compat_check: - /^[\d-]+-stable(-ee)?/ - branches@gitlab-org/gitlab-ee - branches@gitlab/gitlab-ee - allow_failure: yes + allow_failure: no retry: 0 - cache: - key: "ee_compat_check_repo" - paths: - - ee_compat_check/ee-repo/ artifacts: name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}" when: on_failure @@ -453,6 +449,7 @@ db:migrate:reset-mysql: stage: test variables: SETUP_DB: "false" + CREATE_DB_USER: "true" script: - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git checkout -f FETCH_HEAD @@ -478,7 +475,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: @@ -497,6 +494,7 @@ db:rollback-mysql: variables: SIZE: "1" SETUP_DB: "false" + CREATE_DB_USER: "true" script: - git clone https://gitlab.com/gitlab-org/gitlab-test.git /home/git/repositories/gitlab-org/gitlab-test.git @@ -532,7 +530,6 @@ gitlab:assets:compile: NODE_ENV: "production" RAILS_ENV: "production" SETUP_DB: "false" - USE_DB: "false" SKIP_STORAGE_VALIDATION: "true" WEBPACK_REPORT: "true" NO_COMPRESSION: "true" @@ -581,7 +578,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/.scss-lint.yml b/.scss-lint.yml index a855ef3c6e9..dcd4cac780a 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -241,7 +241,7 @@ linters: # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: - enabled: false + enabled: true # Do not use parent selector references (&) when they would otherwise # be unnecessary. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2482f0124bd..f85b78cb277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.1.4 (2017-11-14) + +### Fixed (4 changes) + +- Don't try to create fork network memberships for forks with a missing source. !15366 +- Formats bytes to human reabale number in registry table. +- Prevent error when authorizing an admin-created OAauth application without a set owner. +- Prevents position update for image diff notes. + + +## 10.1.3 (2017-11-10) + +- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization. +- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137 +- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex)) +- [FIXED] Fix issues with forked projects of which the source was deleted. !15150 +- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228 +- [FIXED] Make sure group and project creation is blocked for new users that are external by default. +- [FIXED] Fix arguments Import/Export error importing project merge requests. +- [FIXED] Fix diff parser so it tolerates to diff special markers in the content. +- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table. +- [FIXED] Render 404 when polling commit notes without having permissions. +- [FIXED] Show error message when fast-forward merge is not possible. +- [FIXED] Avoid regenerating the ref path for the environment. +- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests. + ## 10.1.2 (2017-11-08) - [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4f9b378b40f..7f422a161ae 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.52.0 +0.53.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 15a27998172..bea438e9ade 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -3.3.0 +3.3.1 @@ -343,7 +343,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 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ae145ca5f69..dc56e6e8f82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,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 +275,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.51.0) + gitaly-proto (0.52.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -451,11 +452,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 +574,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 +903,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 +959,7 @@ GEM builder expression_parser rinku + with_env (1.1.0) xml-simple (1.1.5) xpath (2.1.0) nokogiri (~> 1.3) @@ -1026,7 +1034,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.51.0) + gitaly-proto (~> 0.52.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1058,7 +1066,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) @@ -1185,4 +1193,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.15.4 + 1.16.0 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/abuse_reports.js b/app/assets/javascripts/abuse_reports.js index 3de192d56eb..d2d3a257c0d 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/abuse_reports.js @@ -1,3 +1,5 @@ +import { truncate } from './lib/utils/text_utility'; + const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; @@ -15,7 +17,7 @@ export default class AbuseReports { if (reportMessage.length > MAX_MESSAGE_LENGTH) { $messageCellElement.data('original-message', reportMessage); $messageCellElement.data('message-truncated', 'true'); - $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH)); + $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH)); } } diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index de9e44cef35..182957113a2 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import Flash from '../../../flash'; import './lists_dropdown'; +import { pluralize } from '../../../lib/utils/text_utility'; const ModalStore = gl.issueBoards.ModalStore; @@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ submitText() { const count = ModalStore.selectedCount(); - return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; + return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`; }, }, methods: { 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/commits.js b/app/assets/javascripts/commits.js index ae6b8902032..9b952ea7b60 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -3,6 +3,8 @@ prefer-template, object-shorthand, prefer-arrow-callback */ /* global Pager */ +import { pluralize } from './lib/utils/text_utility'; + export default (function () { const CommitsList = {}; @@ -86,7 +88,7 @@ export default (function () { // Update commits count in the previous commits header. commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); - $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`); + $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); } gl.utils.localTimeAgo($processedData.find('.js-timeago')); diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 3bed0678350..9a4c9bfcc80 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, prefer-arrow-callback */ import Api from './api'; +import { humanize } from './lib/utils/text_utility'; export default class CreateLabelDropdown { constructor($el, namespacePath, projectPath) { @@ -107,7 +108,7 @@ export default class CreateLabelDropdown { errors = label.message; } else { errors = Object.keys(label.message).map(key => - `${gl.text.humanize(key)} ${label.message[key].join(', ')}`, + `${humanize(key)} ${label.message[key].join(', ')}`, ).join('<br/>'); } diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 8bf9ae17de0..a8cd8c20f8f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ import { __ } from '../locale'; -import '../lib/utils/text_utility'; +import { dasherize } from '../lib/utils/text_utility'; import DEFAULT_EVENT_OBJECTS from './default_event_objects'; const EMPTY_STAGE_TEXTS = { @@ -36,7 +36,7 @@ export default { }); newData.stages.forEach((item) => { - const stageSlug = gl.text.dasherize(item.name.toLowerCase()); + const stageSlug = dasherize(item.name.toLowerCase()); item.active = false; item.isUserAllowed = data.permissions[stageSlug]; item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 44606989395..344b31cf8b7 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,6 +1,6 @@ /* 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 IssuableForm from './issuable_form'; @@ -20,15 +20,14 @@ import groupsSelect from './groups_select'; import NamespaceSelect from './namespace_select'; /* global NewCommitForm */ /* global NewBranchForm */ -/* global Project */ -/* global ProjectAvatar */ +import Project from './project'; +import projectAvatar from './project_avatar'; /* global MergeRequest */ /* global Compare */ /* global CompareAutocomplete */ /* global ProjectFindFile */ -/* global ProjectNew */ -/* global ProjectShow */ -/* global ProjectImport */ +import ProjectNew from './project_new'; +import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; /* global Sidebar */ @@ -91,6 +90,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 +188,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 +198,7 @@ import Diff from './diff'; break; case 'dashboard:issues': case 'dashboard:merge_requests': - new ProjectSelect(); + projectSelect(); initLegacyFilters(); break; case 'groups:issues': @@ -206,7 +207,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 +340,8 @@ import Diff from './diff'; container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); - initChangesDropdown(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': @@ -378,7 +380,7 @@ import Diff from './diff'; initSettingsPanels(); break; case 'projects:imports:show': - new ProjectImport(); + projectImport(); break; case 'projects:pipelines:new': new NewBranchForm($('.js-new-pipeline-form')); @@ -484,7 +486,7 @@ import Diff from './diff'; if ($el.find('.dropdown-group-label').length) { new GroupLabelSubscription($el); } else { - new gl.ProjectLabelSubscription($el); + new ProjectLabelSubscription($el); } }); break; @@ -520,7 +522,7 @@ import Diff from './diff'; // Initialize expandable settings panels initSettingsPanels(); case 'groups:settings:ci_cd:show': - new gl.ProjectVariables(); + new ProjectVariables(); break; case 'ci:lints:create': case 'ci:lints:show': @@ -604,7 +606,7 @@ import Diff from './diff'; break; case 'projects': new Project(); - new ProjectAvatar(); + projectAvatar(); switch (path[1]) { case 'compare': new CompareAutocomplete(); @@ -623,7 +625,6 @@ import Diff from './diff'; case 'show': new Star(); new ProjectNew(); - new ProjectShow(); new NotificationsDropdown(); break; case 'wikis': diff --git a/app/assets/javascripts/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_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index fc0308b81ba..9d25f806c0d 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -2,7 +2,7 @@ import Timeago from 'timeago.js'; import _ from 'underscore'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import '../../lib/utils/text_utility'; +import { humanize } from '../../lib/utils/text_utility'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; import StopComponent from './environment_stop.vue'; @@ -139,7 +139,7 @@ export default { if (this.hasManualActions) { return this.model.last_deployment.manual_actions.map((action) => { const parsedAction = { - name: gl.text.humanize(action.name), + name: humanize(action.name), play_path: action.play_path, playable: action.playable, }; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 5c624b79d45..a642464c920 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -338,7 +338,8 @@ class GfmAutoComplete { let resultantValue = value; if (value && !this.setting.skipSpecialCharacterTest) { const withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) { + const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/; + if (withoutAt && regex.test(withoutAt)) { resultantValue = `${value.charAt()}"${withoutAt}"`; } } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c4202f92443..4e7a6e54f90 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -331,7 +331,7 @@ GitLabDropdown = (function() { if (_this.dropdown.find('.dropdown-toggle-page').length) { selector = ".dropdown-page-one " + selector; } - return $(selector); + return $(selector, this.instance.dropdown); }; })(this), data: (function(_this) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 48cd43d3348..d0f9e6af0f8 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -2,6 +2,7 @@ import GfmAutoComplete from './gfm_auto_complete'; import dropzoneInput from './dropzone_input'; +import textUtils from './lib/utils/text_markdown'; export default class GLForm { constructor(form, enableGFM = false) { @@ -46,7 +47,7 @@ export default class GLForm { } // form and textarea event listeners this.addEventListeners(); - gl.text.init(this.form); + textUtils.init(this.form); // hide discard button this.form.find('.js-note-discard').hide(); this.form.show(); @@ -85,7 +86,7 @@ export default class GLForm { clearEventListeners() { this.textarea.off('focus'); this.textarea.off('blur'); - gl.text.removeListeners(this.form); + textUtils.removeListeners(this.form); } addEventListeners() { diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index acd5730cf3c..7de07e9403d 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ import 'vendor/jquery.waitforimages'; -import '~/lib/utils/text_utility'; +import { addDelimiter } from './lib/utils/text_utility'; import Flash from './flash'; import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; @@ -73,7 +73,7 @@ export default class Issue { let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; - projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); + projectIssuesCounter.text(addDelimiter(numProjectIssues)); if (this.createMergeRequestDropdown) { if (isClosed) { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index d1aa83ea57f..e8ac8d3b5bb 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,11 @@ export default { type: String, required: true, }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, }, data() { const store = new Store({ @@ -157,21 +167,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 +233,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :project-path="projectPath" :project-namespace="projectNamespace" + :show-delete-button="showDeleteButton" /> <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/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 28bf6c67ea5..8bb5c86d567 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -36,6 +36,11 @@ type: String, required: true, }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, }, components: { lockedWarning, @@ -81,6 +86,7 @@ :markdown-docs-path="markdownDocsPath" /> <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/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 07899777a1e..195e2ca6a78 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -172,7 +172,6 @@ export const getSelectedFragment = () => { return documentFragment; }; -// TODO: Update this name, there is a gl.text.insertText function. export const insertText = (target, text) => { // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas const selectionStart = target.selectionStart; @@ -311,6 +310,42 @@ export const setParamInURL = (param, value) => { }; /** + * Given a string of query parameters creates an object. + * + * @example + * `scope=all&page=2` -> { scope: 'all', page: '2'} + * `scope=all` -> { scope: 'all' } + * ``-> {} + * @param {String} query + * @returns {Object} + */ +export const parseQueryStringIntoObject = (query = '') => { + if (query === '') return {}; + + return query + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + Object.assign(acc, { + [val[0]]: decodeURIComponent(val[1]), + }); + return acc; + }, {}); +}; + +export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); + +/** + * Based on the current location and the string parameters provided + * creates a new entry in the history without reloading the page. + * + * @param {String} param + */ +export const historyPushState = (newUrl) => { + window.history.pushState({}, document.title, newUrl); +}; + +/** * Converts permission provided as strings to booleans. * * @param {String} string diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 29fc91733b3..5679b8c9a09 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,6 +2,7 @@ import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; +import { pluralize } from './text_utility'; import { lang, @@ -143,9 +144,9 @@ export function timeIntervalInWords(intervalInSeconds) { let text = ''; if (minutes >= 1) { - text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; + text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`; } else { - text = `${seconds} ${gl.text.pluralize('second', seconds)}`; + text = `${seconds} ${pluralize('second', seconds)}`; } return text; } diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 917a45eb06b..a02c79b787e 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -52,3 +52,31 @@ export function bytesToKiB(number) { export function bytesToMiB(number) { return number / (BYTES_IN_KIB * BYTES_IN_KIB); } + +/** + * Utility function that calculates GiB of the given bytes. + * @param {Number} number + * @returns {Number} + */ +export function bytesToGiB(number) { + return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB); +} + +/** + * Port of rails number_to_human_size + * Formats the bytes in number into a more understandable + * representation (e.g., giving it 1500 yields 1.5 KB). + * + * @param {Number} size + * @returns {String} + */ +export function numberToHumanSize(size) { + if (size < BYTES_IN_KIB) { + return `${size} bytes`; + } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) { + return `${bytesToKiB(size).toFixed(2)} KiB`; + } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) { + return `${bytesToMiB(size).toFixed(2)} MiB`; + } + return `${bytesToGiB(size).toFixed(2)} GiB`; +} diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 1485e900945..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({ @@ -60,7 +62,6 @@ export default class Poll { checkConditions(response) { const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); - if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { this.timeoutID = setTimeout(() => { this.makeRequest(); @@ -102,7 +103,12 @@ export default class Poll { /** * Restarts polling after it has been stoped */ - restart() { + restart(options) { + // update data + if (options && options.data) { + this.options.data = options.data; + } + this.canPoll = true; this.makeRequest(); } diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js new file mode 100644 index 00000000000..2dc9cf0cc29 --- /dev/null +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -0,0 +1,153 @@ +/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ + +const textUtils = {}; + +textUtils.selectedText = function(text, textarea) { + return text.substring(textarea.selectionStart, textarea.selectionEnd); +}; + +textUtils.lineBefore = function(text, textarea) { + var split; + split = text.substring(0, textarea.selectionStart).trim().split('\n'); + return split[split.length - 1]; +}; + +textUtils.lineAfter = function(text, textarea) { + return text.substring(textarea.selectionEnd).trim().split('\n')[0]; +}; + +textUtils.blockTagText = function(text, textArea, blockTag, selected) { + var lineAfter, lineBefore; + lineBefore = this.lineBefore(text, textArea); + lineAfter = this.lineAfter(text, textArea); + if (lineBefore === blockTag && lineAfter === blockTag) { + // To remove the block tag we have to select the line before & after + if (blockTag != null) { + textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); + textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); + } + return selected; + } else { + return blockTag + "\n" + selected + "\n" + blockTag; + } +}; + +textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) { + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; + removedLastNewLine = false; + removedFirstNewLine = false; + currentLineEmpty = false; + + // Remove the first newline + if (selected.indexOf('\n') === 0) { + removedFirstNewLine = true; + selected = selected.replace(/\n+/, ''); + } + + // Remove the last newline + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + + selectedSplit = selected.split('\n'); + + if (!wrap) { + lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); + + // Check whether the current line is empty or consists only of spaces(=handle as empty) + if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) { + currentLineEmpty = true; + } + } + + startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; + + if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { + if (blockTag != null && blockTag !== '') { + insertText = this.blockTagText(text, textArea, blockTag, selected); + } else { + insertText = selectedSplit.map(function(val) { + if (val.indexOf(tag) === 0) { + return "" + (val.replace(tag, '')); + } else { + return "" + tag + val; + } + }).join('\n'); + } + } else { + insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); + } + + if (removedFirstNewLine) { + insertText = '\n' + insertText; + } + + if (removedLastNewLine) { + insertText += '\n'; + } + + if (document.queryCommandSupported('insertText')) { + inserted = document.execCommand('insertText', false, insertText); + } + if (!inserted) { + try { + document.execCommand("ms-beginUndoUnit"); + } catch (error) {} + textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); + try { + document.execCommand("ms-endUndoUnit"); + } catch (error) {} + } + return this.moveCursor(textArea, tag, wrap, removedLastNewLine); +}; + +textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { + var pos; + if (!textArea.setSelectionRange) { + return; + } + if (textArea.selectionStart === textArea.selectionEnd) { + if (wrapped) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } + + if (removedLastNewLine) { + pos -= 1; + } + + return textArea.setSelectionRange(pos, pos); + } +}; + +textUtils.updateText = function(textArea, tag, blockTag, wrap) { + var $textArea, selected, text; + $textArea = $(textArea); + textArea = $textArea.get(0); + text = $textArea.val(); + selected = this.selectedText(text, textArea); + $textArea.focus(); + return this.insertText(textArea, text, tag, blockTag, selected, wrap); +}; + +textUtils.init = function(form) { + var self; + self = this; + return $('.js-md', form).off('click').on('click', function() { + var $this; + $this = $(this); + return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); + }); +}; + +textUtils.removeListeners = function(form) { + return $('.js-md', form).off('click'); +}; + +textUtils.replaceRange = function(s, start, end, substitute) { + return s.substring(0, start) + substitute + s.substring(end); +}; + +export default textUtils; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index f776829f69c..a1475b92c7e 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,18 +1,13 @@ -/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ - -import 'vendor/latinise'; - -var base; -var w = window; -if (w.gl == null) { - w.gl = {}; -} -if ((base = w.gl).text == null) { - base.text = {}; -} -gl.text.addDelimiter = function(text) { - return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; -}; +/** + * Adds a , to a string composed by numbers, at every 3 chars. + * + * 2333 -> 2,333 + * 232324 -> 232,324 + * + * @param {String} text + * @returns {String} + */ +export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); /** * Returns '99+' for numbers bigger than 99. @@ -20,178 +15,43 @@ gl.text.addDelimiter = function(text) { * @param {Number} count * @return {Number|String} */ -export function highCountTrim(count) { - return count > 99 ? '99+' : count; -} - -gl.text.randomString = function() { - return Math.random().toString(36).substring(7); -}; -gl.text.replaceRange = function(s, start, end, substitute) { - return s.substring(0, start) + substitute + s.substring(end); -}; -gl.text.getTextWidth = function(text, font) { - /** - * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. - * - * @param {String} text The text to be rendered. - * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). - * - * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 - */ - // re-use canvas object for better performance - var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); - var context = canvas.getContext('2d'); - context.font = font; - return context.measureText(text).width; -}; -gl.text.selectedText = function(text, textarea) { - return text.substring(textarea.selectionStart, textarea.selectionEnd); -}; -gl.text.lineBefore = function(text, textarea) { - var split; - split = text.substring(0, textarea.selectionStart).trim().split('\n'); - return split[split.length - 1]; -}; -gl.text.lineAfter = function(text, textarea) { - return text.substring(textarea.selectionEnd).trim().split('\n')[0]; -}; -gl.text.blockTagText = function(text, textArea, blockTag, selected) { - var lineAfter, lineBefore; - lineBefore = this.lineBefore(text, textArea); - lineAfter = this.lineAfter(text, textArea); - if (lineBefore === blockTag && lineAfter === blockTag) { - // To remove the block tag we have to select the line before & after - if (blockTag != null) { - textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); - textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); - } - return selected; - } else { - return blockTag + "\n" + selected + "\n" + blockTag; - } -}; -gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; - removedLastNewLine = false; - removedFirstNewLine = false; - currentLineEmpty = false; - - // Remove the first newline - if (selected.indexOf('\n') === 0) { - removedFirstNewLine = true; - selected = selected.replace(/\n+/, ''); - } - - // Remove the last newline - if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { - removedLastNewLine = true; - selected = selected.replace(/\n$/, ''); - } - - selectedSplit = selected.split('\n'); - - if (!wrap) { - lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); - - // Check whether the current line is empty or consists only of spaces(=handle as empty) - if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) { - currentLineEmpty = true; - } - } - - startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; +export const highCountTrim = count => (count > 99 ? '99+' : count); - if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { - if (blockTag != null && blockTag !== '') { - insertText = this.blockTagText(text, textArea, blockTag, selected); - } else { - insertText = selectedSplit.map(function(val) { - if (val.indexOf(tag) === 0) { - return "" + (val.replace(tag, '')); - } else { - return "" + tag + val; - } - }).join('\n'); - } - } else { - insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); - } +/** + * Converts first char to uppercase and replaces undercores with spaces + * @param {String} string + * @requires {String} + */ +export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); - if (removedFirstNewLine) { - insertText = '\n' + insertText; - } +/** + * Adds an 's' to the end of the string when count is bigger than 0 + * @param {String} str + * @param {Number} count + * @returns {String} + */ +export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : ''); - if (removedLastNewLine) { - insertText += '\n'; - } +/** + * Replaces underscores with dashes + * @param {*} str + * @returns {String} + */ +export const dasherize = str => str.replace(/[_\s]+/g, '-'); - if (document.queryCommandSupported('insertText')) { - inserted = document.execCommand('insertText', false, insertText); - } - if (!inserted) { - try { - document.execCommand("ms-beginUndoUnit"); - } catch (error) {} - textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText); - try { - document.execCommand("ms-endUndoUnit"); - } catch (error) {} - } - return this.moveCursor(textArea, tag, wrap, removedLastNewLine); -}; -gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { - var pos; - if (!textArea.setSelectionRange) { - return; - } - if (textArea.selectionStart === textArea.selectionEnd) { - if (wrapped) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; - } +/** + * Removes accents and converts to lower case + * @param {String} str + * @returns {String} + */ +export const slugify = str => str.trim().toLowerCase(); - if (removedLastNewLine) { - pos -= 1; - } +/** + * Truncates given text + * + * @param {String} string + * @param {Number} maxLength + * @returns {String} + */ +export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`; - return textArea.setSelectionRange(pos, pos); - } -}; -gl.text.updateText = function(textArea, tag, blockTag, wrap) { - var $textArea, selected, text; - $textArea = $(textArea); - textArea = $textArea.get(0); - text = $textArea.val(); - selected = this.selectedText(text, textArea); - $textArea.focus(); - return this.insertText(textArea, text, tag, blockTag, selected, wrap); -}; -gl.text.init = function(form) { - var self; - self = this; - return $('.js-md', form).off('click').on('click', function() { - var $this; - $this = $(this); - return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); - }); -}; -gl.text.removeListeners = function(form) { - return $('.js-md', form).off('click'); -}; -gl.text.humanize = function(string) { - return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); -}; -gl.text.pluralize = function(str, count) { - return str + (count > 1 || count === 0 ? 's' : ''); -}; -gl.text.truncate = function(string, maxLength) { - return string.substr(0, (maxLength - 3)) + '...'; -}; -gl.text.dasherize = function(str) { - return str.replace(/[_\s]+/g, '-'); -}; -gl.text.slugify = function(str) { - return str.trim().toLowerCase().latinise(); -}; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 1aa63216baf..17236c91490 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -100,6 +100,10 @@ export function visitUrl(url, external = false) { } } +export function redirectTo(url) { + return window.location.assign(url); +} + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 31c5cfc5e55..cef79eec273 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -30,7 +30,6 @@ import './commit/image_file'; import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; import './lib/utils/pretty_time'; -import './lib/utils/text_utility'; import './lib/utils/url_utility'; // behaviors @@ -70,15 +69,8 @@ import './notifications_dropdown'; import './notifications_form'; import './pager'; import './preview_markdown'; -import './project'; -import './project_avatar'; 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'; diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 6264750a4fb..52315e969d1 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -5,7 +5,6 @@ export default class Members { } addListeners() { - $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); @@ -33,17 +32,6 @@ export default class Members { }); }); } - // eslint-disable-next-line class-methods-use-this - removeRow(e) { - const $target = $(e.target); - - if ($target.hasClass('btn-remove')) { - $target.closest('.member') - .fadeOut(function fadeOutMemberRow() { - $(this).remove(); - }); - } - } formSubmit(e, $el = null) { const $this = e ? $(e.currentTarget) : $el; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index af0658eb668..d30ff12bb59 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages'; import TaskList from './task_list'; import './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; +import { addDelimiter } from './lib/utils/text_utility'; (function() { this.MergeRequest = (function() { @@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper'; const $el = $('.nav-links .js-merge-counter'); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); - $el.text(gl.text.addDelimiter(count)); + $el.text(addDelimiter(count)); }; MergeRequest.prototype.hideCloseButton = function() { 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/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 547140b1a43..19d8e1f49cf 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,7 +1,7 @@ <script> import tooltip from '../../../vue_shared/directives/tooltip'; import icon from '../../../vue_shared/components/icon.vue'; - + import { dasherize } from '../../../lib/utils/text_utility'; /** * Renders either a cancel, retry or play icon pointing to the given path. * TODO: Remove UJS from here and use an async request instead. @@ -39,7 +39,7 @@ computed: { cssClass() { - const actionIconDash = gl.text.dasherize(this.actionIcon); + const actionIconDash = dasherize(this.actionIcon); return `${actionIconDash} js-icon-${actionIconDash}`; }, }, diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue index 73f7e3a0cad..07befd23500 100644 --- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue +++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue @@ -2,16 +2,8 @@ export default { name: 'PipelineNavigationTabs', props: { - scope: { - type: String, - required: true, - }, - count: { - type: Object, - required: true, - }, - paths: { - type: Object, + tabs: { + type: Array, required: true, }, }, @@ -23,68 +15,37 @@ // 0 is valid in a badge, but evaluates to false, we need to check for undefined return count !== undefined; }, + + onTabClick(tab) { + this.$emit('onChangeTab', tab.scope); + }, }, }; </script> <template> <ul class="nav-links scrolling-tabs"> <li - class="js-pipelines-tab-all" - :class="{ active: scope === 'all'}"> - <a :href="paths.allPath"> - All - <span - v-if="shouldRenderBadge(count.all)" - class="badge js-totalbuilds-count"> - {{count.all}} - </span> - </a> - </li> - <li - class="js-pipelines-tab-pending" - :class="{ active: scope === 'pending'}"> - <a :href="paths.pendingPath"> - Pending - <span - v-if="shouldRenderBadge(count.pending)" - class="badge"> - {{count.pending}} - </span> - </a> - </li> - <li - class="js-pipelines-tab-running" - :class="{ active: scope === 'running'}"> - <a :href="paths.runningPath"> - Running - <span - v-if="shouldRenderBadge(count.running)" - class="badge"> - {{count.running}} - </span> - </a> - </li> - <li - class="js-pipelines-tab-finished" - :class="{ active: scope === 'finished'}"> - <a :href="paths.finishedPath"> - Finished + v-for="(tab, i) in tabs" + :key="i" + :class="{ + active: tab.isActive, + }" + > + <a + role="button" + @click="onTabClick(tab)" + :class="`js-pipelines-tab-${tab.scope}`" + > + {{ tab.name }} + <span - v-if="shouldRenderBadge(count.finished)" - class="badge"> - {{count.finished}} + v-if="shouldRenderBadge(tab.count)" + class="badge" + > + {{tab.count}} </span> + </a> </li> - <li - class="js-pipelines-tab-branches" - :class="{ active: scope === 'branches'}"> - <a :href="paths.branchesPath">Branches</a> - </li> - <li - class="js-pipelines-tab-tags" - :class="{ active: scope === 'tags'}"> - <a :href="paths.tagsPath">Tags</a> - </li> </ul> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 3da60e88474..cf241c8ffed 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,10 +1,17 @@ <script> + import _ from 'underscore'; import PipelinesService from '../services/pipelines_service'; import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; + import { + convertPermissionToBoolean, + getParameterByName, + historyPushState, + buildUrlWithCurrentLocation, + parseQueryStringIntoObject, + } from '../../lib/utils/common_utils'; export default { props: { @@ -41,27 +48,18 @@ autoDevopsPath: pipelinesData.helpAutoDevopsPath, newPipelinePath: pipelinesData.newPipelinePath, canCreatePipeline: pipelinesData.canCreatePipeline, - allPath: pipelinesData.allPath, - pendingPath: pipelinesData.pendingPath, - runningPath: pipelinesData.runningPath, - finishedPath: pipelinesData.finishedPath, - branchesPath: pipelinesData.branchesPath, - tagsPath: pipelinesData.tagsPath, hasCi: pipelinesData.hasCi, ciLintPath: pipelinesData.ciLintPath, state: this.store.state, - apiScope: 'all', - pagenum: 1, + scope: getParameterByName('scope') || 'all', + page: getParameterByName('page') || '1', + requestData: {}, }; }, computed: { canCreatePipelineParsed() { return convertPermissionToBoolean(this.canCreatePipeline); }, - scope() { - const scope = getParameterByName('scope'); - return scope === null ? 'all' : scope; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,46 +104,112 @@ hasCiEnabled() { return this.hasCi !== undefined; }, - paths() { - return { - allPath: this.allPath, - pendingPath: this.pendingPath, - finishedPath: this.finishedPath, - runningPath: this.runningPath, - branchesPath: this.branchesPath, - tagsPath: this.tagsPath, - }; - }, - pageParameter() { - return getParameterByName('page') || this.pagenum; - }, - scopeParameter() { - return getParameterByName('scope') || this.apiScope; + + tabs() { + const { count } = this.state; + return [ + { + name: 'All', + scope: 'all', + count: count.all, + isActive: this.scope === 'all', + }, + { + name: 'Pending', + scope: 'pending', + count: count.pending, + isActive: this.scope === 'pending', + }, + { + name: 'Running', + scope: 'running', + count: count.running, + isActive: this.scope === 'running', + }, + { + name: 'Finished', + scope: 'finished', + count: count.finished, + isActive: this.scope === 'finished', + }, + { + name: 'Branches', + scope: 'branches', + isActive: this.scope === 'branches', + }, + { + name: 'Tags', + scope: 'tags', + isActive: this.scope === 'tags', + }, + ]; }, }, created() { this.service = new PipelinesService(this.endpoint); - this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; + this.requestData = { page: this.page, scope: this.scope }; }, methods: { + successCallback(resp) { + return resp.json().then((response) => { + // Because we are polling & the user is interacting verify if the response received + // matches the last request made + if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { + this.store.storeCount(response.count); + this.store.storePagination(resp.headers); + this.setCommonData(response.pipelines); + } + }); + }, /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. + * Handles URL and query parameter changes. + * When the user uses the pagination or the tabs, + * - update URL + * - Make API request to the server with new parameters + * - Update the polling function + * - Update the internal state */ - change(pageNumber) { - const param = setParamInURL('page', pageNumber); + updateContent(parameters) { + // stop polling + this.poll.stop(); + + const queryString = Object.keys(parameters).map((parameter) => { + const value = parameters[parameter]; + // update internal state for UI + this[parameter] = value; + return `${parameter}=${encodeURIComponent(value)}`; + }).join('&'); - gl.utils.visitUrl(param); - return param; + // update polling parameters + this.requestData = parameters; + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + + this.isLoading = true; + // fetch new data + return this.service.getPipelines(this.requestData) + .then((response) => { + this.isLoading = false; + this.successCallback(response); + + // restart polling + this.poll.restart({ data: this.requestData }); + }) + .catch(() => { + this.isLoading = false; + this.errorCallback(); + + // restart polling + this.poll.restart(); + }); }, - successCallback(resp) { - return resp.json().then((response) => { - this.store.storeCount(response.count); - this.store.storePagination(resp.headers); - this.setCommonData(response.pipelines); - }); + onChangeTab(scope) { + this.updateContent({ scope, page: '1' }); + }, + onChangePage(page) { + /* URLS parameters are strings, we need to parse to match types */ + this.updateContent({ scope: this.scope, page: Number(page).toString() }); }, }, }; @@ -154,7 +218,7 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!isLoading && !shouldRenderEmptyState"> + v-if="!shouldRenderEmptyState"> <div class="fade-left"> <i class="fa fa-angle-left" @@ -167,17 +231,17 @@ aria-hidden="true"> </i> </div> + <navigation-tabs - :scope="scope" - :count="state.count" - :paths="paths" + :tabs="tabs" + @onChangeTab="onChangeTab" /> <navigation-controls :new-pipeline-path="newPipelinePath" :has-ci-enabled="hasCiEnabled" :help-page-path="helpPagePath" - :ciLintPath="ciLintPath" + :ci-lint-path="ciLintPath" :can-create-pipeline="canCreatePipelineParsed " /> </div> @@ -188,6 +252,7 @@ label="Loading Pipelines" size="3" v-if="isLoading" + class="prepend-top-20" /> <empty-state @@ -221,8 +286,8 @@ <table-pagination v-if="shouldRenderPagination" - :change="change" - :pageInfo="state.pageInfo" + :change="onChangePage" + :page-info="state.pageInfo" /> </div> </div> diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index fe6602259e2..36b6a5ed376 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,139 +1,131 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global ProjectSelect */ +/* 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 */ import Cookies from 'js-cookie'; +import projectSelect from './project_select'; -(function() { - this.Project = (function() { - function Project() { - const $cloneOptions = $('ul.clone-options-dropdown'); - const $projectCloneField = $('#project_clone'); - const $cloneBtnText = $('a.clone-dropdown-btn span'); +export default class Project { + constructor() { + const $cloneOptions = $('ul.clone-options-dropdown'); + const $projectCloneField = $('#project_clone'); + const $cloneBtnText = $('a.clone-dropdown-btn span'); - const selectedCloneOption = $cloneBtnText.text().trim(); - if (selectedCloneOption.length > 0) { - $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); - } - - $('a', $cloneOptions).on('click', (e) => { - const $this = $(e.currentTarget); - const url = $this.attr('href'); - - e.preventDefault(); - - $('.is-active', $cloneOptions).not($this).removeClass('is-active'); - $this.toggleClass('is-active'); - $projectCloneField.val(url); - $cloneBtnText.text($this.text()); - - return $('.clone').text(url); - }); - // Ref switcher - this.initRefSwitcher(); - $('.project-refs-select').on('change', function() { - return $(this).parents('form').submit(); - }); - $('.hide-no-ssh-message').on('click', function(e) { - Cookies.set('hide_no_ssh_message', 'false'); - $(this).parents('.no-ssh-key-message').remove(); - return e.preventDefault(); - }); - $('.hide-no-password-message').on('click', function(e) { - Cookies.set('hide_no_password_message', 'false'); - $(this).parents('.no-password-message').remove(); - return e.preventDefault(); - }); - this.projectSelectDropdown(); + const selectedCloneOption = $cloneBtnText.text().trim(); + if (selectedCloneOption.length > 0) { + $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - Project.prototype.projectSelectDropdown = function() { - new ProjectSelect(); - $('.project-item-select').on('click', (function(_this) { - return function(e) { - return _this.changeProject($(e.currentTarget).val()); - }; - })(this)); - }; - - Project.prototype.changeProject = function(url) { - return window.location = url; - }; - - Project.prototype.initRefSwitcher = function() { - var refListItem = document.createElement('li'); - var refLink = document.createElement('a'); - - refLink.href = '#'; - - return $('.js-project-refs-dropdown').each(function() { - var $dropdown, selected; - $dropdown = $(this); - selected = $dropdown.data('selected'); - return $dropdown.glDropdown({ - data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { - ref: $dropdown.data('ref'), - search: term - }, - dataType: "json" - }).done(function(refs) { - return callback(refs); - }); - }, - selectable: true, - filterable: true, - filterRemote: true, - filterByText: true, - inputFieldName: $dropdown.data('input-field-name'), - fieldName: $dropdown.data('field-name'), - renderRow: function(ref) { - var li = refListItem.cloneNode(false); - - if (ref.header != null) { - li.className = 'dropdown-header'; - li.textContent = ref.header; - } else { - var link = refLink.cloneNode(false); - - if (ref === selected) { - link.className = 'is-active'; - } - - link.textContent = ref; - link.dataset.ref = ref; - - li.appendChild(link); + $('a', $cloneOptions).on('click', (e) => { + const $this = $(e.currentTarget); + const url = $this.attr('href'); + + e.preventDefault(); + + $('.is-active', $cloneOptions).not($this).removeClass('is-active'); + $this.toggleClass('is-active'); + $projectCloneField.val(url); + $cloneBtnText.text($this.text()); + + return $('.clone').text(url); + }); + // Ref switcher + Project.initRefSwitcher(); + $('.project-refs-select').on('change', function() { + return $(this).parents('form').submit(); + }); + $('.hide-no-ssh-message').on('click', function(e) { + Cookies.set('hide_no_ssh_message', 'false'); + $(this).parents('.no-ssh-key-message').remove(); + return e.preventDefault(); + }); + $('.hide-no-password-message').on('click', function(e) { + Cookies.set('hide_no_password_message', 'false'); + $(this).parents('.no-password-message').remove(); + return e.preventDefault(); + }); + Project.projectSelectDropdown(); + } + + static projectSelectDropdown () { + projectSelect(); + $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); + } + + static changeProject(url) { + return window.location = url; + } + + static initRefSwitcher() { + var refListItem = document.createElement('li'); + var refLink = document.createElement('a'); + + refLink.href = '#'; + + return $('.js-project-refs-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + return $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref'), + search: term, + }, + dataType: 'json', + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterRemote: true, + filterByText: true, + inputFieldName: $dropdown.data('input-field-name'), + fieldName: $dropdown.data('field-name'), + renderRow: function(ref) { + var li = refListItem.cloneNode(false); + + if (ref.header != null) { + li.className = 'dropdown-header'; + li.textContent = ref.header; + } else { + var link = refLink.cloneNode(false); + + if (ref === selected) { + link.className = 'is-active'; } - return li; - }, - id: function(obj, $el) { - return $el.attr('data-ref'); - }, - toggleLabel: function(obj, $el) { - return $el.text().trim(); - }, - clicked: function(options) { - const { e } = options; - e.preventDefault(); - if ($('input[name="ref"]').length) { - var $form = $dropdown.closest('form'); - - var $visit = $dropdown.data('visit'); - var shouldVisit = $visit ? true : $visit; - var action = $form.attr('action'); - var divider = action.indexOf('?') === -1 ? '?' : '&'; - if (shouldVisit) { - gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); - } + link.textContent = ref; + link.dataset.ref = ref; + + li.appendChild(link); + } + + return li; + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(options) { + const { e } = options; + e.preventDefault(); + if ($('input[name="ref"]').length) { + var $form = $dropdown.closest('form'); + + var $visit = $dropdown.data('visit'); + var shouldVisit = $visit ? true : $visit; + var action = $form.attr('action'); + var divider = action.indexOf('?') === -1 ? '?' : '&'; + if (shouldVisit) { + gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); } } - }); + }, }); - }; - - return Project; - })(); -}).call(window); + }); + } +} diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index aabdfbf65e2..56627aa155c 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -1,20 +1,13 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ -(function() { - this.ProjectAvatar = (function() { - function ProjectAvatar() { - $('.js-choose-project-avatar-button').bind('click', function() { - var form; - form = $(this).closest('form'); - return form.find('.js-project-avatar-input').click(); - }); - $('.js-project-avatar-input').bind('change', function() { - var filename, form; - form = $(this).closest('form'); - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find('.js-avatar-filename').text(filename); - }); - } +export default function projectAvatar() { + $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() { + const form = $(this).closest('form'); + return form.find('.js-project-avatar-input').click(); + }); - return ProjectAvatar; - })(); -}).call(window); + $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { + const form = $(this).closest('form'); + // eslint-disable-next-line no-useless-escape + const filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); +} diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 08334bf1ec5..d2d26d6f67e 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,13 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ +import { visitUrl } from './lib/utils/url_utility'; -(function() { - this.ProjectImport = (function() { - function ProjectImport() { - setTimeout(function() { - return gl.utils.visitUrl(location.href); - }, 5000); - } +export default function projectImport() { + setTimeout(() => { + visitUrl(location.href); + }, 5000); +} - return ProjectImport; - })(); -}).call(window); 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/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index e917279947e..14d43e135fe 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -8,6 +8,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import { errorMessages, errorMessagesTypes } from '../constants'; + import { numberToHumanSize } from '../../lib/utils/number_utils'; export default { props: { @@ -41,6 +42,10 @@ return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; }, + formatSize(size) { + return numberToHumanSize(size); + }, + handleDeleteRegistry(registry) { this.deleteRegistry(registry) .then(() => this.fetchList({ repo: this.repo })) @@ -97,7 +102,7 @@ </span> </td> <td> - {{item.size}} + {{formatSize(item.size)}} <template v-if="item.size && item.layers"> · </template> diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js index be290c268b1..120ce96f44d 100644 --- a/app/assets/javascripts/repo/stores/actions.js +++ b/app/assets/javascripts/repo/stores/actions.js @@ -3,7 +3,7 @@ import flash from '../../flash'; import service from '../services'; import * as types from './mutation_types'; -export const redirectToUrl = url => gl.utils.visitUrl(url); +export const redirectToUrl = (_, url) => gl.utils.visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); @@ -84,7 +84,7 @@ export const commitChanges = ({ commit, state, dispatch, getters }, { payload, n flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); if (newMr) { - redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`); + dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`); } else { commit(types.SET_COMMIT_REF, data.id); diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js index b81a70dfd1e..61d9a5af3e3 100644 --- a/app/assets/javascripts/repo/stores/actions/branch.js +++ b/app/assets/javascripts/repo/stores/actions/branch.js @@ -3,16 +3,16 @@ import * as types from '../mutation_types'; import { pushState } from '../utils'; // eslint-disable-next-line import/prefer-default-export -export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch( - rootState.project.id, +export const createNewBranch = ({ state, commit }, branch) => service.createBranch( + state.project.id, { branch, - ref: rootState.currentBranch, + ref: state.currentBranch, }, ).then(res => res.json()) .then((data) => { const branchName = data.name; - const url = location.href.replace(rootState.currentBranch, branchName); + const url = location.href.replace(state.currentBranch, branchName); pushState(url); 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/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index 219ff94924e..13e4cb5717e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -1,5 +1,5 @@ import tooltip from '../../vue_shared/directives/tooltip'; -import '../../lib/utils/text_utility'; +import { pluralize } from '../../lib/utils/text_utility'; export default { name: 'MRWidgetHeader', @@ -14,7 +14,7 @@ export default { return this.mr.divergedCommitsCount > 0; }, commitsText() { - return gl.text.pluralize('commit', this.mr.divergedCommitsCount); + return pluralize('commit', this.mr.divergedCommitsCount); }, branchNameClipboardData() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index f82938aa8a9..1274db2c4c8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -61,7 +61,7 @@ export default { return this.mr.hasCI; }, shouldRenderRelatedLinks() { - return this.mr.relatedLinks; + return !!this.mr.relatedLinks; }, shouldRenderDeployments() { return this.mr.deployments.length; 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/wikis.js b/app/assets/javascripts/wikis.js index a0025ddb598..7a865587444 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,4 +1,5 @@ import bp from './breakpoints'; +import { slugify } from './lib/utils/text_utility'; export default class Wikis { constructor() { @@ -23,7 +24,7 @@ export default class Wikis { if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = gl.text.slugify(slugInput.value); + const slug = slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c334f39f416..66212be1b8f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -34,6 +34,7 @@ @import "framework/modal"; @import "framework/pagination"; @import "framework/panels"; +@import "framework/popup"; @import "framework/secondary-navigation-elements"; @import "framework/selects"; @import "framework/sidebar"; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 6bb096fc5bd..10f9e9b70b0 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -7,29 +7,67 @@ width: 100%; height: 100%; padding-bottom: 25px; - border: 1px solid $border-color; border-radius: $border-radius-default; } } -.blank-state { - padding-top: 20px; - padding-bottom: 20px; +.blank-state-row { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + height: 100%; +} + +.blank-state-welcome { text-align: center; + padding: 20px 0 40px; + + .blank-state-welcome-title { + font-size: 24px; + } + + .blank-state-text { + margin-bottom: 0; + } +} - &.blank-state-welcome { - .blank-state-welcome-title { - font-size: 24px; +.blank-state-link { + display: block; + color: $gl-text-color; + flex: 0 0 100%; + margin-bottom: 15px; + + @media (min-width: $screen-sm-min) { + flex: 0 0 49%; + + &:nth-child(odd) { + margin-right: 5px; } - .blank-state-text { - margin-bottom: 0; + &:nth-child(even) { + margin-left: 5px; } } - .blank-state-icon { - padding-bottom: 20px; + &:hover { + background-color: $gray-light; + text-decoration: none; + color: $gl-text-color; + } +} + +.blank-state { + padding: 20px; + border: 1px solid $border-color; + border-radius: $border-radius-default; + + @media (min-width: $screen-sm-min) { + display: flex; + align-items: center; + padding: 50px 30px; + } + .blank-state-icon { svg { display: block; margin: auto; @@ -38,13 +76,17 @@ .blank-state-title { margin-top: 0; - margin-bottom: 10px; font-size: 18px; } - .blank-state-text { - max-width: $container-text-max-width; - margin: 0 auto $gl-padding; - font-size: 14px; + .blank-state-body { + @media (max-width: $screen-xs-max) { + text-align: center; + margin-top: 20px; + } + + @media (min-width: $screen-sm-min) { + padding-left: 20px; + } } } 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..bbbb73201be 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; 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/files.scss b/app/assets/stylesheets/framework/files.scss index 1247e5e4876..c2a3cd16e67 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -101,13 +101,13 @@ @for $i from 0 through 5 { .legend-box-#{$i} { - background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%); } } @for $i from 1 through 4 { .legend-box-#{$i + 5} { - background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%); } } } @@ -200,13 +200,13 @@ @for $i from 0 through 5 { td.blame-commit-age-#{$i} { - border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%); } } @for $i from 1 through 4 { td.blame-commit-age-#{$i + 5} { - border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%); } } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f0139b5f33a..2218b5705fc 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -129,7 +129,7 @@ margin: 5px 2px 5px -8px; border-radius: $border-radius-default; - svg { + .tanuki-logo { @media (min-width: $screen-sm-min) { margin-right: 8px; } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 16d5edde61e..33012133b66 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -180,3 +180,31 @@ display: none; } } + +@mixin triangle($color, $border-color, $size, $border-size) { + &::before, + &::after { + bottom: 100%; + left: 50%; + border: solid transparent; + content: ''; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + + &::before { + border-color: transparent; + border-bottom-color: $border-color; + border-width: ($size + $border-size); + margin-left: -($size + $border-size); + } + + &::after { + border-color: transparent; + border-bottom-color: $color; + border-width: $size; + margin-left: -$size; + } +} diff --git a/app/assets/stylesheets/framework/popup.scss b/app/assets/stylesheets/framework/popup.scss new file mode 100644 index 00000000000..5c76205095f --- /dev/null +++ b/app/assets/stylesheets/framework/popup.scss @@ -0,0 +1,15 @@ +.popup { + @include triangle( + $gray-lighter, + $gray-darker, + $popup-triangle-size, + $popup-triangle-border-size + ); + + padding: $gl-padding; + background-color: $gray-lighter; + border: 1px solid $gray-darker; + border-radius: $border-radius-default; + box-shadow: 0 5px 8px $popup-box-shadow-color; + position: relative; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8ab48e4844f..cb2a237f574 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e; $gl-text-color-secondary: #707070; $gl-text-color-tertiary: #949494; $gl-text-color-quaternary: #d6d6d6; -$gl-text-color-inverted: rgba(255, 255, 255, 1.0); +$gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-green: $green-600; $gl-text-green-hover: $green-700; @@ -486,8 +486,8 @@ $callout-success-color: $green-700; /* * Commit Page */ -$commit-max-width-marker-color: rgba(0, 0, 0, 0.0); -$commit-message-text-area-bg: rgba(0, 0, 0, 0.0); +$commit-max-width-marker-color: rgba(0, 0, 0, 0); +$commit-message-text-area-bg: rgba(0, 0, 0, 0); /* * Common @@ -719,3 +719,10 @@ Image Commenting cursor */ $image-comment-cursor-left-offset: 12; $image-comment-cursor-top-offset: 30; + +/* +Popup +*/ +$popup-triangle-size: 15px; +$popup-triangle-border-size: 1px; +$popup-box-shadow-color: rgba(90, 90, 90, 0.05); 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/environments.scss b/app/assets/stylesheets/pages/environments.scss index 26c5f093c6b..b0795353ec1 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -173,7 +173,7 @@ .prometheus-graph-overlay { fill: none; - opacity: 0.0; + opacity: 0; pointer-events: all; } 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 14514b2f193..1e6992cb65e 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -7,7 +7,7 @@ .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { - opacity: 1.0; + opacity: 1; filter: alpha(opacity = 100); } } @@ -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/settings.scss b/app/assets/stylesheets/pages/settings.scss index 8b9b47a41bc..5d630c7d61e 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -249,3 +249,22 @@ } } } + +.modal-doorkeepr-auth, +.doorkeeper-app-form { + .scope-description { + color: $theme-gray-700; + } +} + +.modal-doorkeepr-auth { + .modal-body { + padding: $gl-padding; + } +} + +.doorkeeper-app-form { + .scope-description { + margin: 0 0 5px 17px; + } +} 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..2087fe81411 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) - sessionless_sign_in(user) + return try(:authenticated_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) + # 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) + sessionless_sign_in(user) if user end def log_exception(exception) 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/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 6cca9f95618..4311f9d4db9 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -92,15 +92,7 @@ module LfsRequest end def storage_project - @storage_project ||= begin - result = project - - # TODO: Make this go to the fork_network root immeadiatly - # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 - result = result.fork_source while result.forked? - - result - end + @storage_project ||= project.lfs_storage_project end def objects 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/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/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 1b985ea9763..1c4c09c772f 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController before_action :authorize_read_build!, only: [:index, :show, :status, :raw, :trace] before_action :authorize_update_build!, - except: [:index, :show, :status, :raw, :trace, :cancel_all] + except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] + before_action :authorize_erase_build!, only: [:erase] layout 'project' @@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController return access_denied! unless can?(current_user, :update_build, build) end + def authorize_erase_build! + return access_denied! unless can?(current_user, :erase_build, build) + end + def build @build ||= project.builds.find(params[:id]) .present(current_user: current_user) 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_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/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb index b8f52e31926..c3f5358b577 100644 --- a/app/finders/autocomplete_users_finder.rb +++ b/app/finders/autocomplete_users_finder.rb @@ -45,7 +45,7 @@ class AutocompleteUsersFinder def find_users return users_from_project if project - return group.users if group + return group.users_with_parents if group return User.all if current_user User.none diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 24c07f3dc70..b46ec5e5350 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -36,6 +36,7 @@ class IssuableFinder iids label_name milestone_title + my_reaction_emoji non_archived project_id scope 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/icons_helper.rb b/app/helpers/icons_helper.rb index ec779c1c447..c6a83f21ceb 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -23,10 +23,17 @@ module IconsHelper render "shared/icons/#{icon_name}.svg", size: size end + def sprite_icon_path + # SVG Sprites currently don't work across domains, so in the case of a CDN + # we have to set the current path deliberately to prevent addition of asset_host + sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host + ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url) + end + def sprite_icon(icon_name, size: nil, css_class: nil) css_classes = size ? "s#{size}" : "" css_classes << " #{css_class}" unless css_class.blank? - content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{image_path('icons.svg')}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) + content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) end def audit_icon(names, options = {}) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 2c85d7d7720..6636e4d2362 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 diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index d7df9bb06d2..b78d3072186 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -4,8 +4,11 @@ module NamespacesHelper end def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) - groups = current_user.owned_groups + current_user.masters_groups - users = [current_user.namespace] + groups = current_user.manageable_groups + .joins(:route) + .includes(:route) + .order('routes.path') + users = [current_user.namespace] unless extra_group.nil? || extra_group.is_a?(Group) extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group' 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/ci/build.rb b/app/models/ci/build.rb index 6ca46ae89c1..1d9f367183e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -192,6 +192,10 @@ module Ci project.build_timeout end + def triggered_by?(current_user) + user == current_user + end + # A slugified version of the build ref, suitable for inclusion in URLs and # domain names. Rules: # @@ -313,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 fcbe3d2b67b..3ded675bec0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -66,8 +66,8 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition created: :pending - transition [:success, :failed, :canceled, :skipped] => :running + transition [:created, :skipped] => :pending + transition [:success, :failed, :canceled] => :running end event :run do @@ -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) + scope :internal, -> { where(source: internal_sources) } - if ref - where(ref: ref, id: max_id.where(ref: ref)) - else - where(id: max_id) - end + # 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 - scope :internal, -> { where(source: internal_sources) } 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 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..a31ebe9cc87 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -80,6 +80,7 @@ class Commit @raw = raw_commit @project = project + @statuses = {} end def id @@ -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 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/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 710fc1ed647..7026f565706 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -86,6 +86,14 @@ module Milestoneish false end + def total_issue_time_spent + @total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent) + end + + def human_total_issue_time_spent + Gitlab::TimeTrackingFormatter.output(total_issue_time_spent) + end + private def count_issues_by_state(user) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index d88a92dc027..ae5f138a920 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -18,7 +18,8 @@ class DiffNote < Note validate :positions_complete validate :verify_supported - before_validation :set_original_position, :update_position, on: :create + before_validation :set_original_position, on: :create + before_validation :update_position, on: :create, if: :on_text? before_validation :set_line_code after_save :keep_around_commits 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 3b3c7fb7f8b..b5abc8f57b0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -262,10 +262,6 @@ class Issue < ActiveRecord::Base true end - def update_project_counter_caches? - state_changed? || confidential_changed? - end - def update_project_counter_caches Projects::OpenIssuesCountService.new(project).refresh_cache end 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/lfs_object.rb b/app/models/lfs_object.rb index b7cf96abe83..fc586fa216e 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base mount_uploader :file, LfsObjectUploader - def storage_project(project) - if project && project.forked? - storage_project(project.forked_from_project) - else - project - end - end - def project_allowed_access?(project) - projects.exists?(storage_project(project).id) + projects.exists?(project.lfs_storage_project.id) end def self.destroy_unreferenced diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index efd8cca2947..f1a5cc73e83 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -865,7 +865,19 @@ class MergeRequest < ActiveRecord::Base # def all_commit_shas if persisted? - column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).limit(10_000).pluck('sha') + # MySQL doesn't support LIMIT in a subquery. + diffs_relation = + if Gitlab::Database.postgresql? + merge_request_diffs.order(id: :desc).limit(100) + else + merge_request_diffs + end + + column_shas = MergeRequestDiffCommit + .where(merge_request_diff: diffs_relation) + .limit(10_000) + .pluck('sha') + serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas) (column_shas + serialised_shas).uniq @@ -946,10 +958,6 @@ class MergeRequest < ActiveRecord::Base true end - def update_project_counter_caches? - state_changed? - end - def update_project_counter_caches Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache 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/pages_domain.rb b/app/models/pages_domain.rb index 2e824cda525..8de42ff9d2e 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -65,12 +65,18 @@ class PagesDomain < ActiveRecord::Base def expired? return false unless x509 + current = Time.new current < x509.not_before || x509.not_after < current end + def expiration + x509&.not_after + end + def subject return unless x509 + x509.subject.to_s end @@ -98,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 @@ -105,6 +112,7 @@ class PagesDomain < ActiveRecord::Base def x509 return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) rescue OpenSSL::X509::CertificateError nil @@ -112,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.rb b/app/models/project.rb index bae16b6b2af..894ded2a9f6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -704,10 +704,6 @@ class Project < ActiveRecord::Base import_type == 'gitea' end - def github_import? - import_type == 'github' - end - def check_limit unless creator.can_create_project? || namespace.kind == 'group' projects_limit = creator.projects_limit @@ -1047,6 +1043,18 @@ class Project < ActiveRecord::Base forked_from_project || fork_network&.root_project end + def lfs_storage_project + @lfs_storage_project ||= begin + result = self + + # TODO: Make this go to the fork_network root immeadiatly + # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 + result = result.fork_source while result&.forked? + + result || self + end + end + def personal? !group end 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_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 217f753f05f..fa7b3f2bcaf 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -25,7 +25,7 @@ class PrometheusService < MonitoringService end def description - 'Prometheus monitoring' + s_('PrometheusService|Prometheus monitoring') end def self.to_param @@ -38,8 +38,8 @@ class PrometheusService < MonitoringService type: 'text', name: 'api_url', title: 'API URL', - placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/', - help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.', + placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'), + help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'), required: true } ] 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 6bdee538172..8a6a8377de9 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 @@ -662,6 +667,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 +996,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 @@ -1062,6 +1064,10 @@ class Repository blob_data_at(sha, path) end + def fetch_ref(source_repository, source_ref:, target_ref:) + raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) + end + private # TODO Generice finder, later split this on finders by Ref or Oid diff --git a/app/models/user.rb b/app/models/user.rb index f436efd604f..0329d094d09 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 @@ -624,7 +624,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 +888,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) @@ -921,7 +927,16 @@ class User < ActiveRecord::Base end def manageable_namespaces - @manageable_namespaces ||= [namespace] + owned_groups + masters_groups + @manageable_namespaces ||= [namespace] + manageable_groups + end + + def manageable_groups + union = Gitlab::SQL::Union.new([owned_groups.select(:id), + masters_groups.select(:id)]) + arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql) + owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union)) + + Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants end def namespaces @@ -1110,6 +1125,7 @@ class User < ActiveRecord::Base # override, from Devise::Validatable def password_required? return false if internal? + super end @@ -1127,6 +1143,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/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 984e5482288..1ab391a5a9d 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -10,6 +10,15 @@ module Ci end end - rule { protected_ref }.prevent :update_build + condition(:owner_of_job) do + can?(:developer_access) && @subject.triggered_by?(@user) + end + + rule { protected_ref }.policy do + prevent :update_build + prevent :erase_build + end + + rule { can?(:master_access) | owner_of_job }.enable :erase_build end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 8c89eea607f..69d46f5ec14 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity expose :pipeline, using: PipelineEntity expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity - expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build| + expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build| erase_project_job_path(project, build) end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 5f47592e4ad..9d52b8d9752 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -3,7 +3,6 @@ class IssueEntity < IssuableEntity expose :state expose :deleted_at - expose :branch_name expose :confidential expose :discussion_locked expose :assignees, using: API::Entities::UserBasic 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/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 120af8c1e61..a9813d774bb 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -1,5 +1,7 @@ module Ci class PipelineTriggerService < BaseService + include Gitlab::Utils::StrongMemoize + def execute if trigger_from_token create_pipeline_from_trigger(trigger_from_token) @@ -26,9 +28,9 @@ module Ci end def trigger_from_token - return @trigger if defined?(@trigger) - - @trigger = Ci::Trigger.find_by_token(params[:token].to_s) + strong_memoize(:trigger) do + Ci::Trigger.find_by_token(params[:token].to_s) + end end def create_pipeline_variables!(pipeline) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 68b49d880f7..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) @@ -187,7 +188,7 @@ class IssuableBaseService < BaseService # We have to perform this check before saving the issuable as Rails resets # the changed fields upon calling #save. - update_project_counters = issuable.project && issuable.update_project_counter_caches? + update_project_counters = issuable.project && update_project_counter_caches?(issuable) if issuable.with_transaction_returning_status { issuable.save } # We do not touch as it will affect a update on updated_at field @@ -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 @@ -288,4 +294,8 @@ class IssuableBaseService < BaseService # override if needed def execute_hooks(issuable, action = 'open', params = {}) end + + def update_project_counter_caches?(issuable) + issuable.state_changed? + end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 735257c4779..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) @@ -45,5 +45,9 @@ module Issues params.delete(:assignee_ids) end end + + def update_project_counter_caches?(issue) + super || issue.confidential_changed? + end end end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 43b539ded53..997d247be46 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -19,6 +19,7 @@ module Labels # We skipped validations during creation. Let's run them now, after deleting conflicting labels raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid? + new_label end end diff --git a/app/services/merge_requests/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 0fa1e46ae3d..cedfcb50e09 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -20,15 +20,7 @@ module MergeRequests @merge_request = merge_request - unless @merge_request.mergeable? - return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true) - end - - @source = find_merge_source - - unless @source - return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true) - end + error_check! merge_request.in_locked_state do if commit @@ -44,6 +36,19 @@ module MergeRequests private + def error_check! + error = + if @merge_request.should_be_rebased? + 'Only fast-forward merge is allowed for your project. Please update your source branch' + elsif !@merge_request.mergeable? + 'Merge request is not mergeable' + elsif !source + 'No source for merge' + end + + raise MergeError, error if error + end + def commit message = params[:commit_message] || merge_request.merge_commit_message @@ -103,8 +108,8 @@ module MergeRequests merge_request.to_reference(full: true) end - def find_merge_source - merge_request.diff_head_sha + def source + @source ||= @merge_request.diff_head_sha end end end 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/import_service.rb b/app/services/projects/import_service.rb index c950da44aba..c3b11341b4d 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -11,13 +11,11 @@ module Projects # supported by an importer class (`Gitlab::GithubImport::ParallelImporter` # for example). def async? - return false unless has_importer? - - !!importer_class.try(:async?) + has_importer? && !!importer_class.try(:async?) end def execute - add_repository_to_project unless project.gitlab_project_import? + add_repository_to_project import_data @@ -29,6 +27,14 @@ module Projects private def add_repository_to_project + if project.external_import? && !unknown_url? + raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) + end + + # We should skip the repository for a GitHub import or GitLab project import, + # because these importers fetch the project repositories for us. + return if has_importer? && importer_class.try(:imports_repository?) + if unknown_url? # In this case, we only want to import issues, not a repository. create_repository @@ -44,12 +50,6 @@ module Projects end def import_repository - raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) - - # We should return early for a GitHub import because the new GitHub - # importer fetch the project repositories for us. - return if project.github_import? - begin if project.gitea_import? fetch_repository @@ -88,7 +88,7 @@ module Projects end def importer_class - Gitlab::ImportSources.importer(project.import_type) + @importer_class ||= Gitlab::ImportSources.importer(project.import_type) end def has_importer? 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/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/index.html.haml b/app/views/admin/runners/index.html.haml index 4965dffab9d..4f60be698e9 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -64,7 +64,7 @@ %th Projects %th Jobs %th Tags - %th Last contact + %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) %th - @runners.each do |runner| 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/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 57544559824..573a4b93d67 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -1,33 +1,41 @@ -.blank-state - .blank-state-icon - = custom_icon("add_new_user", size: 50) - .blank-state-body - %h3.blank-state-title - Add user - %p.blank-state-text - Add your team members and others to GitLab. - = link_to new_admin_user_path, class: "btn btn-new" do - New user +.blank-state-row + = link_to new_project_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("add_new_project", size: 50) + .blank-state-body + %h3.blank-state-title + Create a project + %p.blank-state-text + Projects are where you store your code, access issues, wiki and other features of GitLab. -.blank-state - .blank-state-icon - = custom_icon("configure_server", size: 50) - .blank-state-body - %h3.blank-state-title - Configure GitLab - %p.blank-state-text - Make adjustments to how your GitLab instance is set up. - = link_to admin_root_path, class: "btn btn-new" do - Configure + - if current_user.can_create_group? + = link_to admin_root_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("add_new_group", size: 50) + .blank-state-body + %h3.blank-state-title + Create a group + %p.blank-state-text + Groups are a great way to organize projects and people. -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("add_new_group", size: 50) - .blank-state-body - %h3.blank-state-title - Create a group - %p.blank-state-text - Groups are a great way to organize projects and people. - = link_to new_group_path, class: "btn btn-new" do - New group + = link_to new_admin_user_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("add_new_user", size: 50) + .blank-state-body + %h3.blank-state-title + Add people + %p.blank-state-text + Add your team members and others to GitLab. + + = link_to admin_root_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("configure_server", size: 50) + .blank-state-body + %h3.blank-state-title + Configure GitLab + %p.blank-state-text + Make adjustments to how your GitLab instance is set up. diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml index a93a3415ee1..8d5bddbb288 100644 --- a/app/views/dashboard/projects/_blank_state_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -1,48 +1,58 @@ - public_project_count = ProjectsFinder.new(current_user: current_user).execute.count -- if current_user.can_create_group? - .blank-state - .blank-state-icon - = custom_icon("add_new_group", size: 50) - .blank-state-body - %h3.blank-state-title - Create a group for several dependent projects. - %p.blank-state-text - Groups are the best way to manage projects and members. - = link_to new_group_path, class: "btn btn-new" do - New group +.blank-state-row + - if current_user.can_create_project? + = link_to new_project_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("add_new_project", size: 50) + .blank-state-body + %h3.blank-state-title + Create a project + %p.blank-state-text + Projects are where you store your code, access issues, wiki and other features of GitLab. + - else + .blank-state + .blank-state-icon + = custom_icon("add_new_project", size: 50) + .blank-state-body + %h3.blank-state-title + Create a project + %p.blank-state-text + If you are added to a project, it will be displayed here. -.blank-state - .blank-state-icon - = custom_icon("add_new_project", size: 50) - .blank-state-body - %h3.blank-state-title - Create a project - %p.blank-state-text - - if current_user.can_create_project? - You don't have access to any projects right now. - You can create up to - %strong= number_with_delimiter(current_user.projects_limit) - = succeed "." do - = "project".pluralize(current_user.projects_limit) - - else - If you are added to a project, it will be displayed here. - - if current_user.can_create_project? - = link_to new_project_path, class: "btn btn-new" do - New project + - if current_user.can_create_group? + = link_to new_group_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("add_new_group", size: 50) + .blank-state-body + %h3.blank-state-title + Create a group + %p.blank-state-text + Groups are the best way to manage projects and members. -- if public_project_count > 0 - .blank-state - .blank-state-icon - = custom_icon("globe", size: 50) - .blank-state-body - %h3.blank-state-title - Explore public projects - %p.blank-state-text - There are - = number_with_delimiter(public_project_count) - public projects on this server. - Public projects are an easy way to allow - everyone to have read-only access. - = link_to trending_explore_projects_path, class: "btn btn-new" do - Browse projects + - if public_project_count > 0 + = link_to trending_explore_projects_path, class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("globe", size: 50) + .blank-state-body + %h3.blank-state-title + Explore public projects + %p.blank-state-text + There are + = number_with_delimiter(public_project_count) + public projects on this server. + Public projects are an easy way to allow + everyone to have read-only access. + + = link_to "https://docs.gitlab.com/", class: "blank-state-link" do + .blank-state + .blank-state-icon + = custom_icon("lightbulb", size: 50) + .blank-state-body + %h3.blank-state-title + Learn more about GitLab + %p.blank-state-text + Take a look at the documentation to discover all of GitLab's capabilities. diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index ad3fac6d164..18a82feb189 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,12 +1,13 @@ -.row.blank-state-parent-container +.blank-state-parent-container .section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" } .container.section-body - .blank-state.blank-state-welcome - %h2.blank-state-welcome-title - Welcome to GitLab - %p.blank-state-text - Code, test, and deploy together - - if current_user.admin? - = render "blank_state_admin_welcome" - - else - = render "blank_state_welcome" + .row + .blank-state-welcome + %h2.blank-state-welcome-title + Welcome to GitLab + %p.blank-state-text + Code, test, and deploy together + - if current_user.admin? + = render "blank_state_admin_welcome" + - else + = render "blank_state_welcome" diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index b3313c7c985..cf0e0de1ca4 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -1,4 +1,4 @@ -= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| += form_for application, url: doorkeeper_submit_path(application), html: { role: 'form', class: 'doorkeeper-app-form' } do |f| = form_errors(application) .form-group diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 8ba88906714..6d9c6b5572a 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,5 +1,5 @@ %main{ :role => "main" } - .modal-no-backdrop + .modal-no-backdrop.modal-doorkeepr-auth .modal-content .modal-header %h3.page-title @@ -16,14 +16,26 @@ %strong= @pre_auth.client.name will allow them to interact with GitLab as an admin as well. Proceed with caution. %p - You are about to authorize + An application called = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer' - to use your account. - - if @pre_auth.scopes + is requesting access to your GitLab account. + + - auth_app_owner = @pre_auth.client.application.owner + - if auth_app_owner + This application was created by + = succeed "." do + = link_to auth_app_owner.name, user_path(auth_app_owner) + + Please note that this application is not provided by GitLab and you should verify its authenticity before + allowing access. + - if @pre_auth.scopes + %p This application will be able to: %ul - @pre_auth.scopes.each do |scope| - %li= t scope, scope: [:doorkeeper, :scopes] + %li + %strong= t scope, scope: [:doorkeeper, :scopes] + .scope-description= t scope, scope: [:doorkeeper, :scope_desc] .form-actions.text-right = form_tag oauth_authorization_path, method: :delete, class: 'inline' do = hidden_field_tag :client_id, @pre_auth.client.uid 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/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml index a80518f7986..3e36da31ea3 100644 --- a/app/views/notify/_note_email.html.haml +++ b/app/views/notify/_note_email.html.haml @@ -1,10 +1,15 @@ - discussion = @note.discussion if @note.part_of_discussion? +- diff_discussion = discussion&.diff_discussion? +- on_image = discussion.on_image? if diff_discussion + - if discussion + - phrase_end_char = on_image ? "." : ":" + %p.details - = succeed ':' do + = succeed phrase_end_char do = link_to @note.author_name, user_url(@note.author) - - if discussion.diff_discussion? + - if diff_discussion - if discussion.new_discussion? started a new discussion - else @@ -21,7 +26,7 @@ %p.details #{link_to @note.author_name, user_url(@note.author)} commented: -- if discussion&.diff_discussion? +- if diff_discussion && !on_image = content_for :head do = stylesheet_link_tag 'mailers/highlighted_diff_email' 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/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b9fec8af4d7..c64eb506412 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 diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index ce0e3872240..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', @@ -71,7 +71,7 @@ class: 'js-raw-link-controller has-tooltip controllers-buttons' do = icon('file-text-o') - - if can?(current_user, :update_build, @project) && @build.erasable? + - if @build.erasable? && can?(current_user, :erase_build, @build) = link_to erase_project_job_path(@project, @build), method: :post, data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, 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/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index f8627a3818b..b2e71cff6ce 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -9,12 +9,6 @@ "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "new-pipeline-path" => new_project_pipeline_path(@project), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, - "all-path" => project_pipelines_path(@project), - "pending-path" => project_pipelines_path(@project, scope: :pending), - "running-path" => project_pipelines_path(@project, scope: :running), - "finished-path" => project_pipelines_path(@project, scope: :finished), - "branches-path" => project_pipelines_path(@project, scope: :branches), - "tags-path" => project_pipelines_path(@project, scope: :tags), "has-ci" => @repository.gitlab_ci_yml, "ci-lint-path" => ci_lint_path } } diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index ea91e8af70e..f53b81cada6 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -2,7 +2,7 @@ .create_access_levels-container = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-create wide', - dropdown_class: 'dropdown-menu-selectable', + dropdown_class: 'dropdown-menu-selectable capitalize-header', data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }}) = render 'projects/protected_tags/shared/create_protected_tag' diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index d8e11500964..b0cb5ce5e8f 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -4,42 +4,39 @@ .row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring .col-lg-3 %h4.prepend-top-0 - Metrics + = s_('PrometheusService|Metrics') %p - Metrics are automatically configured and monitored - based on a library of metrics from popular exporters. - = link_to 'More information', help_page_path('user/project/integrations/prometheus') + = s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.') + = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') .col-lg-9 .panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } } .panel-heading %h3.panel-title - Monitored + = s_('PrometheusService|Monitored') %span.badge.js-monitored-count 0 .panel-body .loading-metrics.text-center.js-loading-metrics = icon('spinner spin 3x', class: 'metrics-load-spinner') - %p Finding and configuring metrics... + %p + = s_('PrometheusService|Finding and configuring metrics...') .empty-metrics.text-center.hidden.js-empty-metrics = custom_icon('icon_empty_metrics') - %p No metrics are being monitored. To start monitoring, deploy to an environment. - = link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do - View environments + %p + = s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.') + = link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success' %ul.list-unstyled.metrics-list.hidden.js-metrics-list .panel.panel-default.hidden.js-panel-missing-env-vars .panel-heading %h3.panel-title = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') - Missing environment variable + = s_('PrometheusService|Missing environment variable') %span.badge.js-env-var-count 0 .panel-body.hidden .flash-container .flash-notice .flash-text - To set up automatic monitoring, add the environment variable - %code - $CI_ENVIRONMENT_SLUG - to exporter’s queries. - = link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels') + = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe + = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels') %ul.list-unstyled.metrics-list.js-missing-var-metrics-list 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/icons/_add_new_project.svg b/app/views/shared/icons/_add_new_project.svg index 3c1e15453df..cf8762944ca 100644 --- a/app/views/shared/icons/_add_new_project.svg +++ b/app/views/shared/icons/_add_new_project.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg> diff --git a/app/views/shared/icons/_icon_hourglass.svg b/app/views/shared/icons/_icon_hourglass.svg new file mode 100644 index 00000000000..fe7e497ce13 --- /dev/null +++ b/app/views/shared/icons/_icon_hourglass.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></svg> diff --git a/app/views/shared/icons/_lightbulb.svg b/app/views/shared/icons/_lightbulb.svg new file mode 100644 index 00000000000..2fcc4c65f99 --- /dev/null +++ b/app/views/shared/icons/_lightbulb.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg> diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 951b4dd7b36..2c27dd638a7 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -104,7 +104,6 @@ class: 'btn btn-remove prepend-left-10' - else = link_to member, - remote: true, method: :delete, data: { confirm: remove_member_message(member) }, class: 'btn btn-remove prepend-left-10', diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index f03e0ab154c..4f51455c26e 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -85,6 +85,22 @@ Closed: = milestone.issues_visible_to_user(current_user).closed.count + .block.time_spent + .sidebar-collapsed-icon + = custom_icon('icon_hourglass') + %span.collapsed-milestone-total-time-spent + - if milestone.human_total_issue_time_spent + = milestone.human_total_issue_time_spent + - else + = _("None") + .title.hide-collapsed + = _("Total issue time spent") + .value.hide-collapsed + - if milestone.human_total_issue_time_spent + %span.bold= milestone.human_total_issue_time_spent + - else + %span.no-value= _("No time spent") + .block.merge-requests .sidebar-collapsed-icon %strong diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 8bbaf431536..ae437dd16d6 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -7,3 +7,4 @@ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" = label_tag ("#{prefix}_scopes_#{scope}"), scope %span= t(scope, scope: [:doorkeeper, :scopes]) + .scope-description= t scope, scope: [:doorkeeper, :scope_desc] 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/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 89ae17cef37..afc47fc63d6 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker include Sidekiq::Worker include DedicatedSidekiqQueue + LOG_TIME_THRESHOLD = 90 # seconds + def perform(project_id, user_id, oldrev, newrev, ref) project = Project.find_by(id: project_id) return unless project @@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker user = User.find_by(id: user_id) return unless user - MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) + # TODO: remove this benchmarking when we have rich logging + time = Benchmark.measure do + MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) + end + + args_log = [ + "elapsed=#{time.real}", + "project_id=#{project_id}", + "user_id=#{user_id}", + "oldrev=#{oldrev}", + "newrev=#{newrev}", + "ref=#{ref}" + ].join(',') + + Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD 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/32098-pipelines-navigation.yml b/changelogs/unreleased/32098-pipelines-navigation.yml new file mode 100644 index 00000000000..925c92b6be8 --- /dev/null +++ b/changelogs/unreleased/32098-pipelines-navigation.yml @@ -0,0 +1,6 @@ +--- +title: Stop reloading the page when using pagination and tabs - use API calls - in + Pipelines table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml new file mode 100644 index 00000000000..d61bbf2e355 --- /dev/null +++ b/changelogs/unreleased/33338-internationalization-support-for-prometheus-service-configuration.yml @@ -0,0 +1,5 @@ +--- +title: Add internationalization support for the prometheus integration +merge_request: 33338 +author: +type: other 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/3615-improve-welcome-screen.yml b/changelogs/unreleased/3615-improve-welcome-screen.yml new file mode 100644 index 00000000000..862efddb162 --- /dev/null +++ b/changelogs/unreleased/3615-improve-welcome-screen.yml @@ -0,0 +1,5 @@ +--- +title: Reorganize welcome page for new users +merge_request: +author: +type: other diff --git a/changelogs/unreleased/38075_allow_refernce_integer_labels.yml b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml new file mode 100644 index 00000000000..b5342d4adf8 --- /dev/null +++ b/changelogs/unreleased/38075_allow_refernce_integer_labels.yml @@ -0,0 +1,5 @@ +--- +title: Fix errors when selecting numeric-only labels in the labels autocomplete selector +merge_request: 14607 +author: haseebeqx +type: fixed diff --git a/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml b/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml deleted file mode 100644 index c7e840f0723..00000000000 --- a/changelogs/unreleased/38385-gpg-tooltips-not-working-in-safari.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix GPG signature popup info in Safari and Firefox -merge_request: 15228 -author: -type: fixed 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/39335-add-time-spend-to-milestones.yml b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml new file mode 100644 index 00000000000..41a43418cbf --- /dev/null +++ b/changelogs/unreleased/39335-add-time-spend-to-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Add total time spent to milestones +merge_request: 15116 +author: George Andrinopoulos +type: added diff --git a/changelogs/unreleased/39436-pages-api-administrative.yml b/changelogs/unreleased/39436-pages-api-administrative.yml new file mode 100644 index 00000000000..f38bbbd479c --- /dev/null +++ b/changelogs/unreleased/39436-pages-api-administrative.yml @@ -0,0 +1,5 @@ +--- +title: Add administrative endpoint to list all pages domains +merge_request: 15160 +author: Travis Miller +type: added 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/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml new file mode 100644 index 00000000000..056afe43010 --- /dev/null +++ b/changelogs/unreleased/39602-move-update-project-counter-caches-out-of-issues-merge-requests.yml @@ -0,0 +1,5 @@ +--- +title: Move update_project_counter_caches? out of issue and merge request +merge_request: 15300 +author: George Andrinopoulos +type: other diff --git a/changelogs/unreleased/39704_fix_webhooks_log_time.yml b/changelogs/unreleased/39704_fix_webhooks_log_time.yml deleted file mode 100644 index 1234663e66b..00000000000 --- a/changelogs/unreleased/39704_fix_webhooks_log_time.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix webhooks recent deliveries -merge_request: 15146 -author: Alexander Randa (@randaalex) -type: fixed diff --git a/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml new file mode 100644 index 00000000000..580b97241e7 --- /dev/null +++ b/changelogs/unreleased/39884-fix-pipeline-transition-with-single-manual-action.yml @@ -0,0 +1,6 @@ +--- +title: Fix pipeline status transition for single manual job. This would also fix pipeline + duration becuse it is depending on status transition +merge_request: 15251 +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/40068-runner-sorting-regression.yml b/changelogs/unreleased/40068-runner-sorting-regression.yml new file mode 100644 index 00000000000..6a2bd59d6d6 --- /dev/null +++ b/changelogs/unreleased/40068-runner-sorting-regression.yml @@ -0,0 +1,5 @@ +--- +title: Revert a regression on runners sorting (!15134) +merge_request: 15341 +author: Takuya Noguchi +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/40161-extra-margin-on-svg-logo-in-header.yml b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml new file mode 100644 index 00000000000..fdaa90f0d5d --- /dev/null +++ b/changelogs/unreleased/40161-extra-margin-on-svg-logo-in-header.yml @@ -0,0 +1,5 @@ +--- +title: Remove extra margin from wordmark in header +merge_request: +author: +type: fixed 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/add-typescript.yml b/changelogs/unreleased/add-typescript.yml deleted file mode 100644 index a048f240855..00000000000 --- a/changelogs/unreleased/add-typescript.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds typescript support -merge_request: -author: -type: added 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/bvl-refresh-member-listing-on-removal.yml b/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml new file mode 100644 index 00000000000..48b4051711c --- /dev/null +++ b/changelogs/unreleased/bvl-refresh-member-listing-on-removal.yml @@ -0,0 +1,5 @@ +--- +title: Don't use JS to delete memberships from projects and groups +merge_request: 15344 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml new file mode 100644 index 00000000000..1114d429dec --- /dev/null +++ b/changelogs/unreleased/bvl-subgroup-in-dropdowns.yml @@ -0,0 +1,5 @@ +--- +title: Make sure a user can add projects to subgroups they have access to +merge_request: 15294 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-unlink-fixes.yml b/changelogs/unreleased/bvl-unlink-fixes.yml deleted file mode 100644 index 685d78f479d..00000000000 --- a/changelogs/unreleased/bvl-unlink-fixes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix issues with forked projects of which the source was deleted -merge_request: 15150 -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/cleanup-issues-schema.yml b/changelogs/unreleased/cleanup-issues-schema.yml new file mode 100644 index 00000000000..9f5fb0bdf82 --- /dev/null +++ b/changelogs/unreleased/cleanup-issues-schema.yml @@ -0,0 +1,5 @@ +--- +title: Clean up schema of the "issues" table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml b/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml deleted file mode 100644 index 42bcf9b1edd..00000000000 --- a/changelogs/unreleased/dm-block-group-and-project-creation-when-external-by-default.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Make sure group and project creation is blocked for new users that are external - by default -merge_request: -author: -type: fixed 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/enable-scss-lint-unnecessary-mantissa.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml new file mode 100644 index 00000000000..1049e94f312 --- /dev/null +++ b/changelogs/unreleased/enable-scss-lint-unnecessary-mantissa.yml @@ -0,0 +1,5 @@ +--- +title: Enable UnnecessaryMantissa in scss-lint +merge_request: 15255 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml b/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml new file mode 100644 index 00000000000..32cdfba4eec --- /dev/null +++ b/changelogs/unreleased/fix-502-mrs-with-lots-of-versions.yml @@ -0,0 +1,6 @@ +--- +title: Ensure merge requests with lots of version don't time out when searching for + pipelines +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/fix-filter-by-my-reaction.yml b/changelogs/unreleased/fix-filter-by-my-reaction.yml new file mode 100644 index 00000000000..8bf91ddf893 --- /dev/null +++ b/changelogs/unreleased/fix-filter-by-my-reaction.yml @@ -0,0 +1,5 @@ +--- +title: Fix filter by my reaction is not working +merge_request: 15345 +author: Hiroyuki Sato +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-import-export-arguments.yml b/changelogs/unreleased/fix-import-export-arguments.yml deleted file mode 100644 index eee87e313ea..00000000000 --- a/changelogs/unreleased/fix-import-export-arguments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix arguments Import/Export error importing project merge requests -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml new file mode 100644 index 00000000000..198116f34aa --- /dev/null +++ b/changelogs/unreleased/fix-sm-31771-do-not-allow-jobs-to-be-erased-new.yml @@ -0,0 +1,5 @@ +--- +title: Only owner or master can erase jobs +merge_request: 15216 +author: +type: changed diff --git a/changelogs/unreleased/fix-subgroup-autocomplete.yml b/changelogs/unreleased/fix-subgroup-autocomplete.yml new file mode 100644 index 00000000000..4baa2b02f77 --- /dev/null +++ b/changelogs/unreleased/fix-subgroup-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Fix user autocomplete in subgroups +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix_diff_parsing.yml b/changelogs/unreleased/fix_diff_parsing.yml deleted file mode 100644 index 7a26b4f9ff5..00000000000 --- a/changelogs/unreleased/fix_diff_parsing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix diff parser so it tolerates to diff special markers in the content -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml deleted file mode 100644 index a1685497331..00000000000 --- a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table -merge_request: -author: -type: fixed 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_39176.yml b/changelogs/unreleased/issue_39176.yml deleted file mode 100644 index 6255b51c094..00000000000 --- a/changelogs/unreleased/issue_39176.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Render 404 when polling commit notes without having permissions -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/issue_39238.yml b/changelogs/unreleased/issue_39238.yml new file mode 100644 index 00000000000..75a4969ca9e --- /dev/null +++ b/changelogs/unreleased/issue_39238.yml @@ -0,0 +1,5 @@ +--- +title: Fix image diff notification email from showing wrong content +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml deleted file mode 100644 index 0205d9626b1..00000000000 --- a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix cancel button not working while uploading on the new issue page -merge_request: 15137 -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/pawel-disable_nfs_metrics_checks_39730.yml b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml deleted file mode 100644 index 556d7d069d3..00000000000 --- a/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove Filesystem check metrics that use too much CPU to handle requests -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/sh-fix-environment-slug-generation.yml b/changelogs/unreleased/sh-fix-environment-slug-generation.yml deleted file mode 100644 index 8a9c670c52c..00000000000 --- a/changelogs/unreleased/sh-fix-environment-slug-generation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Avoid regenerating the ref path for the environment -merge_request: -author: -type: fixed 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/text-utils.yml b/changelogs/unreleased/text-utils.yml new file mode 100644 index 00000000000..b95bb82fe01 --- /dev/null +++ b/changelogs/unreleased/text-utils.yml @@ -0,0 +1,5 @@ +--- +title: Export text utils functions as es6 module and add tests +merge_request: +author: +type: other 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/update-merge-worker-metrics.yml b/changelogs/unreleased/update-merge-worker-metrics.yml new file mode 100644 index 00000000000..c733675926a --- /dev/null +++ b/changelogs/unreleased/update-merge-worker-metrics.yml @@ -0,0 +1,5 @@ +--- +title: Add performance logging to UpdateMergeRequestsWorker. +merge_request: 15360 +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/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/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 0da6b14c29e..b1c71095d4f 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -62,7 +62,15 @@ en: read_user: Read the authenticated user's personal information openid: Authenticate using OpenID Connect sudo: Perform API actions as any user in the system (if the authenticated user is an admin) - + scope_desc: + api: + Full access to GitLab as the user, including read/write on all their groups and projects + read_user: + Read-only access to the user's profile information, like username, public email and full name + openid: + The ability to authenticate using GitLab, and read-only access to the user's profile information + sudo: + Access to the Sudo feature, to perform API actions as any user in the system (only available for admins) flash: applications: create: diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 190eeb59a2c..601a86490d4 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -145,7 +145,7 @@ - container_memory_usage_bytes weight: 1 queries: - - query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024' + - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024' label: Average unit: MB - title: "CPU Utilization" @@ -154,8 +154,6 @@ - container_cpu_usage_seconds_total weight: 1 queries: - - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100' - label: CPU - unit: "%" - series: - - label: cpu + - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100' + label: Average + unit: "%"
\ No newline at end of file diff --git a/config/webpack.config.js b/config/webpack.config.js index 67d7cae3ccf..f7a7182a627 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -108,10 +108,6 @@ var config = { loader: 'vue-loader', }, { - test: /\.ts$/, - loader: 'ts-loader', - }, - { test: /\.svg$/, loader: 'raw-loader', }, @@ -256,7 +252,7 @@ var config = { ], resolve: { - extensions: ['.js', '.ts'], + extensions: ['.js'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 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/20171106132212_issues_confidential_not_null.rb b/db/migrate/20171106132212_issues_confidential_not_null.rb new file mode 100644 index 00000000000..c959d2dd938 --- /dev/null +++ b/db/migrate/20171106132212_issues_confidential_not_null.rb @@ -0,0 +1,23 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class IssuesConfidentialNotNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + class Issue < ActiveRecord::Base + self.table_name = 'issues' + end + + def up + Issue.where('confidential IS NULL').update_all(confidential: false) + + change_column_null :issues, :confidential, false + end + + def down + # There's no way / point to revert this. + end +end diff --git a/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb b/db/migrate/20171106135924_issues_milestone_id_foreign_key.rb new file mode 100644 index 00000000000..e6a780d0964 --- /dev/null +++ b/db/migrate/20171106135924_issues_milestone_id_foreign_key.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 IssuesMilestoneIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + + def self.with_orphaned_milestones + where('NOT EXISTS (SELECT true FROM milestones WHERE milestones.id = issues.milestone_id)') + end + end + + def up + Issue.with_orphaned_milestones.each_batch(of: 100) do |batch| + batch.update_all(milestone_id: nil) + end + + add_concurrent_foreign_key( + :issues, + :milestones, + column: :milestone_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:issues, column: :milestone_id) + end +end diff --git a/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb b/db/migrate/20171106150657_issues_updated_by_id_foreign_key.rb new file mode 100644 index 00000000000..3b8844d7d9f --- /dev/null +++ b/db/migrate/20171106150657_issues_updated_by_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 IssuesUpdatedByIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + + def self.with_orphaned_updaters + where('NOT EXISTS (SELECT true FROM users WHERE users.id = issues.updated_by_id)') + .where('updated_by_id IS NOT NULL') + end + end + + def up + Issue.with_orphaned_updaters.each_batch(of: 100) do |batch| + batch.update_all(updated_by_id: nil) + end + + # This index is only used for foreign keys, and those in turn will always + # specify a value. As such we can add a WHERE condition to make the index + # smaller. + add_concurrent_index(:issues, :updated_by_id, where: 'updated_by_id IS NOT NULL') + + add_concurrent_foreign_key( + :issues, + :users, + column: :updated_by_id, + on_delete: :nullify + ) + end + + def down + remove_foreign_key_without_error(:issues, column: :updated_by_id) + remove_concurrent_index(:issues, :updated_by_id) + end +end diff --git a/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb new file mode 100644 index 00000000000..8d2ceb8cc18 --- /dev/null +++ b/db/migrate/20171106151218_issues_moved_to_id_foreign_key.rb @@ -0,0 +1,44 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class IssuesMovedToIdForeignKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + + def self.with_orphaned_moved_to_issues + where('NOT EXISTS (SELECT true FROM issues WHERE issues.id = issues.moved_to_id)') + .where('moved_to_id IS NOT NULL') + end + end + + def up + Issue.with_orphaned_moved_to_issues.each_batch(of: 100) do |batch| + batch.update_all(moved_to_id: nil) + end + + add_concurrent_foreign_key( + :issues, + :issues, + column: :moved_to_id, + on_delete: :nullify + ) + + # We're using a partial index here so we only index the data we actually + # care about. + add_concurrent_index(:issues, :moved_to_id, where: 'moved_to_id IS NOT NULL') + end + + def down + remove_foreign_key_without_error(:issues, column: :moved_to_id) + remove_concurrent_index(:issues, :moved_to_id) + end +end diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb new file mode 100644 index 00000000000..3d08225c96d --- /dev/null +++ b/db/migrate/20171106154015_remove_issues_branch_name.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveIssuesBranchName < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + remove_column :issues, :branch_name, :string + end +end diff --git a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb new file mode 100644 index 00000000000..e4bed778695 --- /dev/null +++ b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb @@ -0,0 +1,37 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + NEW_INDEX_NAME = 'idx_issues_on_project_id_and_due_date_and_id_and_state_partial' + OLD_INDEX_NAME = 'index_issues_on_project_id_and_due_date_and_id_and_state' + + disable_ddl_transaction! + + def up + add_concurrent_index( + :issues, + [:project_id, :due_date, :id, :state], + where: 'due_date IS NOT NULL', + name: NEW_INDEX_NAME + ) + + # We set the column name to nil as otherwise Rails will ignore the custom + # index name and remove the wrong index. + remove_concurrent_index(:issues, nil, name: OLD_INDEX_NAME) + end + + def down + add_concurrent_index( + :issues, + [:project_id, :due_date, :id, :state], + name: OLD_INDEX_NAME + ) + + remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME) + end +end diff --git a/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb new file mode 100644 index 00000000000..ad540b1e509 --- /dev/null +++ b/db/migrate/20171106171453_add_timezone_to_issues_closed_at.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTimezoneToIssuesClosedAt < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + change_column_type_concurrently(:issues, :closed_at, :datetime_with_timezone) + end + + def down + cleanup_concurrent_column_type_change(:issues, :closed_at) + 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..2965e580c84 --- /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 + MergeRequest.with_orphaned_source_projects.each_batch(of: 100) do |batch| + batch.update_all(source_project_id: nil) + end + + # 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) + + 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/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb new file mode 100644 index 00000000000..88dd8f89ba6 --- /dev/null +++ b/db/post_migrate/20171106180641_cleanup_add_timezone_to_issues_closed_at.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupAddTimezoneToIssuesClosedAt < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_type_change(:issues, :closed_at) + end + + # rubocop:disable Migration/Datetime + def down + change_column_type_concurrently(:issues, :closed_at, :datetime) + 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 c60cb729b75..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: 20171106101200) 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: 20171106101200) 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| @@ -817,13 +826,12 @@ ActiveRecord::Schema.define(version: 20171106101200) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "branch_name" t.text "description" t.integer "milestone_id" t.string "state" t.integer "iid" t.integer "updated_by_id" - t.boolean "confidential", default: false + t.boolean "confidential", default: false, null: false t.datetime "deleted_at" t.date "due_date" t.integer "moved_to_id" @@ -832,11 +840,11 @@ ActiveRecord::Schema.define(version: 20171106101200) do t.text "description_html" t.integer "time_estimate" t.integer "relative_position" - t.datetime "closed_at" t.integer "cached_markdown_version" t.datetime "last_edited_at" t.integer "last_edited_by_id" t.boolean "discussion_locked" + t.datetime_with_timezone "closed_at" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -845,13 +853,15 @@ ActiveRecord::Schema.define(version: 20171106101200) do add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree + add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree - add_index "issues", ["project_id", "due_date", "id", "state"], name: "index_issues_on_project_id_and_due_date_and_id_and_state", using: :btree + add_index "issues", ["project_id", "due_date", "id", "state"], name: "idx_issues_on_project_id_and_due_date_and_id_and_state_partial", where: "(due_date IS NOT NULL)", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} + add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree create_table "keys", force: :cascade do |t| t.integer "user_id" @@ -1030,7 +1040,7 @@ ActiveRecord::Schema.define(version: 20171106101200) 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" @@ -1070,6 +1080,7 @@ ActiveRecord::Schema.define(version: 20171106101200) 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 @@ -1078,6 +1089,7 @@ ActiveRecord::Schema.define(version: 20171106101200) 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 @@ -1937,8 +1949,11 @@ ActiveRecord::Schema.define(version: 20171106101200) do add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade + add_foreign_key "issues", "issues", column: "moved_to_id", name: "fk_a194299be1", on_delete: :nullify + add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify + add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade @@ -1952,7 +1967,13 @@ ActiveRecord::Schema.define(version: 20171106101200) 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/pages_domains.md b/doc/api/pages_domains.md index 51962595e33..50685f335f7 100644 --- a/doc/api/pages_domains.md +++ b/doc/api/pages_domains.md @@ -4,6 +4,31 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages] The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature. +## List all pages domains + +Get a list of all pages domains. The user must have admin permissions. + +```http +GET /pages/domains +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/pages/domains +``` + +```json +[ + { + "domain": "ssl.domain.example", + "url": "https://ssl.domain.example", + "certificate": { + "expired": false, + "expiration": "2020-04-12T14:32:00.000Z" + } + } +] +``` + ## List pages domains Get a list of project pages domains. The user must have permissions to view pages domains. diff --git a/doc/api/services.md b/doc/api/services.md index 08a2bee1518..08df26db3ec 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -490,6 +490,41 @@ Remove all previously JIRA settings from a project. DELETE /projects/:id/services/jira ``` +## Kubernetes + +Kubernetes / Openshift integration + +### Create/Edit Kubernetes service + +Set Kubernetes service for a project. + +``` +PUT /projects/:id/services/kubernetes +``` + +Parameters: + +- `namespace` (**required**) - The Kubernetes namespace to use +- `api_url` (**required**) - The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com +- `token` (**required**) - The service token to authenticate against the Kubernetes cluster with +- `ca_pem` (optional) - A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format) + +### Delete Kubernetes service + +Delete Kubernetes service for a project. + +``` +DELETE /projects/:id/services/kubernetes +``` + +### Get Kubernetes service settings + +Get Kubernetes service settings for a project. + +``` +GET /projects/:id/services/kubernetes +``` + ## Slack slash commands Ability to receive slash commands from a Slack chat instance. 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..cef62618a3c 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -29,7 +29,7 @@ Please use the following function inside JS to render an icon : 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. # 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/licensing.md b/doc/development/licensing.md index 902b1c74a42..274923c2d43 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed ## Automated Testing -In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition. +In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition. -There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. +There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. -Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. +Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually. ### License Finder commands diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 899be9eae4b..ba82babb38a 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as possible to avoid conflicts with big chunks of HAML code that that are not fun to resolve when you add the indentation in the equation. +### Assets + +#### gitlab-svgs + +Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs). + --- [Return to Development documentation](README.md) 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/installation.md b/doc/install/installation.md index 2a004152d5e..4efe911b778 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -299,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-1-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab -**Note:** You can change `10-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 1cfdabac248..28308fc905c 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -321,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet You can override the Helm chart used by bundling up a chart into your project repo or by specifying a project variable: -- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml` +- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml` file in it, Auto DevOps will detect the chart and use it instead of the [default one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app). This can be a great way to control exactly how your application is deployed. diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md new file mode 100644 index 00000000000..9e0d8f79522 --- /dev/null +++ b/doc/update/10.1-to-10.2.md @@ -0,0 +1,360 @@ +--- +comments: false +--- + +# From 10.1 to 10.2 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz +echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz +cd ruby-2.3.5 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-2-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-2-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-1-stable:config/gitlab.yml.example origin/10-2-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-1-stable:lib/support/nginx/gitlab-ssl origin/10-2-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-1-stable:lib/support/nginx/gitlab origin/10-2-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-1-stable:lib/support/init.d/gitlab.default.example origin/10-2-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.0) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-2-stable/lib/support/init.d/gitlab.default.example 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/permissions.md b/doc/user/permissions.md index c03700a3501..b9532bf897f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under |---------------------------------------|-----------------|-------------|----------|--------| | See commits and jobs | ✓ | ✓ | ✓ | ✓ | | Retry or cancel job | | ✓ | ✓ | ✓ | +| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ | | Remove project | | | ✓ | ✓ | | Create project | | | ✓ | ✓ | | Change project configuration | | | ✓ | ✓ | @@ -261,5 +262,6 @@ only. [^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner [^5]: Only if user is not external one. [^6]: Only if user is a member of the project. +[^7]: Only if the build was triggered by the user [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [new-mod]: project/new_ci_build_permissions_model.md 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/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 518683965e8..a6673fa2a00 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -13,8 +13,8 @@ integration services must be enabled. | Name | Query | | ---- | ----- | -| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 | -| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 | +| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 | +| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 | ## Configuring Prometheus to monitor for Kubernetes node metrics diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index b8dd96087f1..43713855e26 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -21,7 +21,7 @@ want to add. --- -Select the user and the [permission level](../../user/permissions.md) +Select the user and the [permission level](../../permissions.md) that you'd like to give the user. Note that you can select more than one user. ![Give user permissions](img/add_user_give_permissions.png) 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/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 318e054e978..c623a516c47 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I should see additional file lines' do - page.within @diff.parent do + page.within @diff.query_scope do expect(first('.new_line').text).not_to have_content "..." end end 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 b9c7d443f6c..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,6 +39,8 @@ module API # Helper Methods for Grape Endpoint module HelperMethods + include Gitlab::Auth::UserAuthFinders + def find_current_user! user = find_user_from_access_token || find_user_from_warden return unless user @@ -51,76 +50,8 @@ module API user end - def access_token - return @access_token if defined?(@access_token) - - @access_token = find_oauth_access_token || find_personal_access_token - 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. @@ -143,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 @@ -153,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( @@ -184,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/entities.rb b/lib/api/entities.rb index a382db92e8d..16ae99b5c6c 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1042,6 +1042,11 @@ module API expose :value end + class PagesDomainCertificateExpiration < Grape::Entity + expose :expired?, as: :expired + expose :expiration + end + class PagesDomainCertificate < Grape::Entity expose :subject expose :expired?, as: :expired @@ -1049,12 +1054,23 @@ module API expose :certificate_text end + class PagesDomainBasic < Grape::Entity + expose :domain + expose :url + expose :certificate, + as: :certificate_expiration, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: PagesDomainCertificateExpiration do |pages_domain| + pages_domain + end + end + class PagesDomain < Grape::Entity expose :domain expose :url expose :certificate, - if: ->(pages_domain, _) { pages_domain.certificate? }, - using: PagesDomainCertificate do |pages_domain| + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: PagesDomainCertificate do |pages_domain| pages_domain end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5f9b94cc89c..b26c61ab8da 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -155,6 +155,11 @@ module API end end + def authenticated_with_full_private_access! + authenticate! + forbidden! unless current_user.full_private_access? + end + def authenticated_as_admin! authenticate! forbidden! unless current_user.admin? @@ -190,6 +195,10 @@ module API not_found! unless user_project.pages_available? end + def require_pages_config_enabled! + not_found! unless Gitlab.config.pages.enabled + end + def can?(object, action, subject = :global) Ability.allowed?(object, action, subject) end @@ -389,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/jobs.rb b/lib/api/jobs.rb index 3c1c412ba42..a116ab3c9bd 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -136,7 +136,7 @@ module API authorize_update_builds! build = find_build!(params[:job_id]) - authorize!(:update_build, build) + authorize!(:erase_build, build) return forbidden!('Job is not erasable!') unless build.erasable? build.erase(erased_by: current_user) diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 259f3f34068..d7b613a717e 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -4,7 +4,6 @@ module API before do authenticate! - require_pages_enabled! end after_validation do @@ -29,10 +28,31 @@ module API end end + resource :pages do + before do + require_pages_config_enabled! + authenticated_with_full_private_access! + end + + desc "Get all pages domains" do + success Entities::PagesDomainBasic + end + params do + use :pagination + end + get "domains" do + present paginate(PagesDomain.all), with: Entities::PagesDomainBasic + end + end + params do requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: { id: %r{[^/]+} } do + before do + require_pages_enabled! + end + desc 'Get all pages domains' do success Entities::PagesDomain end 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/v3/builds.rb b/lib/api/v3/builds.rb index f493fd7c7ec..fa0bef39602 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -169,7 +169,7 @@ module API authorize_update_builds! build = get_build!(params[:build_id]) - authorize!(:update_build, build) + authorize!(:erase_build, build) return forbidden!('Build is not erasable!') unless build.erasable? build.erase(erased_by: current_user) 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/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/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb index c88eb9783ed..67a39d28944 100644 --- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb +++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb @@ -51,10 +51,20 @@ module Gitlab FROM projects WHERE forked_project_links.forked_from_project_id = projects.id ) + AND NOT EXISTS ( + SELECT true + FROM forked_project_links AS parent_links + WHERE parent_links.forked_to_project_id = forked_project_links.forked_from_project_id + AND NOT EXISTS ( + SELECT true + FROM projects + WHERE parent_links.forked_from_project_id = projects.id + ) + ) AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} MISSING_MEMBERS - ForkNetworkMember.count_by_sql(count_sql) > 0 + ForkedProjectLink.count_by_sql(count_sql) > 0 end def log(message) 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/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/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb index 27a95764dc1..f7276a380dc 100644 --- a/lib/gitlab/checks/lfs_integrity.rb +++ b/lib/gitlab/checks/lfs_integrity.rb @@ -15,7 +15,10 @@ module Gitlab return false unless new_lfs_pointers.present? - existing_count = @project.lfs_objects.where(oid: new_lfs_pointers.map(&:lfs_oid)).count + existing_count = @project.lfs_storage_project + .lfs_objects + .where(oid: new_lfs_pointers.map(&:lfs_oid)) + .count existing_count != new_lfs_pointers.count end 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/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..bd5039fb87e 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 diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index ab94ba8a73a..e36d5410431 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -72,7 +72,7 @@ module Gitlab # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, # it would be created from `start_branch_name`. - # If `start_project` is passed, and the branch doesn't exist, + # If `start_repository` is passed, and the branch doesn't exist, # it would try to find the commits from it instead of current repository. def with_branch( branch_name, @@ -80,15 +80,13 @@ module Gitlab start_repository: repository, &block) - # Refactoring aid - unless start_repository.is_a?(Gitlab::Git::Repository) - raise "expected a Gitlab::Git::Repository, got #{start_repository}" - end + Gitlab::Git.check_namespace!(start_repository) + start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) start_branch_name = nil if start_repository.empty_repo? if start_branch_name && !start_repository.branch_exists?(start_branch_name) - raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}" + raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}" end update_branch_with_hooks(branch_name) do diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb new file mode 100644 index 00000000000..3685aa20669 --- /dev/null +++ b/lib/gitlab/git/remote_repository.rb @@ -0,0 +1,82 @@ +module Gitlab + module Git + # + # When a Gitaly call involves two repositories instead of one we cannot + # assume that both repositories are on the same Gitaly server. In this + # case we need to make a distinction between the repository that the + # call is being made on (a Repository instance), and the "other" + # repository (a RemoteRepository instance). This is the reason why we + # have the RemoteRepository class in Gitlab::Git. + # + # When you make changes, be aware that gitaly-ruby sub-classes this + # class. + # + class RemoteRepository + attr_reader :path, :relative_path, :gitaly_repository + + def initialize(repository) + @relative_path = repository.relative_path + @gitaly_repository = repository.gitaly_repository + + # These instance variables will not be available in gitaly-ruby, where + # we have no disk access to this repository. + @repository = repository + @path = repository.path + end + + def empty_repo? + # We will override this implementation in gitaly-ruby because we cannot + # use '@repository' there. + @repository.empty_repo? + end + + def commit_id(revision) + # We will override this implementation in gitaly-ruby because we cannot + # use '@repository' there. + @repository.commit(revision)&.sha + end + + def branch_exists?(name) + # We will override this implementation in gitaly-ruby because we cannot + # use '@repository' there. + @repository.branch_exists?(name) + end + + # Compares self to a Gitlab::Git::Repository. This implementation uses + # 'self.gitaly_repository' so that it will also work in the + # GitalyRemoteRepository subclass defined in gitaly-ruby. + def same_repository?(other_repository) + gitaly_repository.storage_name == other_repository.storage && + gitaly_repository.relative_path == other_repository.relative_path + end + + def fetch_env + gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) + gitaly_address = gitaly_client.address(storage) + gitaly_token = gitaly_client.token(storage) + + request = Gitaly::SSHUploadPackRequest.new(repository: gitaly_repository) + env = { + 'GITALY_ADDRESS' => gitaly_address, + 'GITALY_PAYLOAD' => request.to_json, + 'GITALY_WD' => Dir.pwd, + 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack" + } + env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present? + + env + end + + private + + # Must return an object that responds to 'address' and 'storage'. + def gitaly_client + Gitlab::GitalyClient + end + + def storage + gitaly_repository.storage_name + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d236e1b03e6..ab3892dd50d 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -58,7 +58,7 @@ module Gitlab # Rugged repo object attr_reader :rugged - attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver + attr_reader :storage, :gl_repository, :relative_path # This initializer method is only used on the client side (gitlab-ce). # Gitaly-ruby uses a different initializer. @@ -66,7 +66,6 @@ module Gitlab @storage = storage @relative_path = relative_path @gl_repository = gl_repository - @gitaly_resolver = Gitlab::GitalyClient storage_path = Gitlab.config.repositories.storages[@storage]['path'] @path = File.join(storage_path, @relative_path) @@ -105,7 +104,7 @@ module Gitlab end def exists? - Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| + Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| if enabled gitaly_repository_client.exists? else @@ -985,6 +984,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 @@ -1014,23 +1017,22 @@ module Gitlab def with_repo_branch_commit(start_repository, start_branch_name) Gitlab::Git.check_namespace!(start_repository) + start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) return yield nil if start_repository.empty_repo? - if start_repository == self + if start_repository.same_repository?(self) yield commit(start_branch_name) else - start_commit = start_repository.commit(start_branch_name) - - return yield nil unless start_commit + start_commit_id = start_repository.commit_id(start_branch_name) - sha = start_commit.sha + return yield nil unless start_commit_id - if branch_commit = commit(sha) + if branch_commit = commit(start_commit_id) yield branch_commit else with_repo_tmp_commit( - start_repository, start_branch_name, sha) do |tmp_commit| + start_repository, start_branch_name, start_commit_id) do |tmp_commit| yield tmp_commit end end @@ -1087,6 +1089,9 @@ module Gitlab end def fetch_ref(source_repository, source_ref:, target_ref:) + Gitlab::Git.check_namespace!(source_repository) + source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository) + message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled| if is_enabled gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref) @@ -1375,6 +1380,7 @@ module Gitlab end return nil unless tmp_entry.type == :tree + tmp_entry = tmp_entry[dir] end end @@ -1495,6 +1501,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 @@ -1561,6 +1568,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 @@ -1620,22 +1628,9 @@ module Gitlab end def gitaly_fetch_ref(source_repository, source_ref:, target_ref:) - gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) - gitaly_address = gitaly_resolver.address(source_repository.storage) - gitaly_token = gitaly_resolver.token(source_repository.storage) - - request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository) - env = { - 'GITALY_ADDRESS' => gitaly_address, - 'GITALY_PAYLOAD' => request.to_json, - 'GITALY_WD' => Dir.pwd, - 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack" - } - env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present? - args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref}) - run_git(args, env: env) + run_git(args, env: source_repository.fetch_env) end def gitaly_ff_merge(user, source_sha, target_branch) 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 5d36f16abd4..d4a53d32c28 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -58,8 +58,14 @@ module Gitlab end end - def pages - gollum_wiki.pages.map { |gollum_page| new_page(gollum_page) } + 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(limit: limit) + end + end end def page(title:, version: nil, dir: nil) @@ -82,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 @@ -104,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 @@ -120,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) @@ -179,6 +219,10 @@ module Gitlab Gitlab::Git::WikiFile.new(gollum_file) end + 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) gitaly_wiki_client.write_page(name, format, content, commit_details) end @@ -204,6 +248,12 @@ module Gitlab Gitlab::Git::WikiFile.new(wiki_file) end + + def gitaly_get_all_pages + gitaly_wiki_client.get_all_pages.map do |wiki_page, version| + Gitlab::Git::WikiPage.new(wiki_page, version) + end + end end end end 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..e8a2215959d 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -137,6 +137,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 8226278d5f6..7339468e911 100644 --- a/lib/gitlab/gitaly_client/wiki_page.rb +++ b/lib/gitlab/gitaly_client/wiki_page.rb @@ -1,16 +1,16 @@ 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 + super - FIELDS.each do |field| - instance_variable_set("@#{field}", params[field]) - end + # All gRPC strings in a response are frozen, so we get an unfrozen + # version here so appending to `raw_data` doesn't blow up. + @raw_data = @raw_data.dup end def historical? diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index 1a668338f57..c8f065f5881 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -81,28 +81,24 @@ module Gitlab ) response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_page, request) - wiki_page = version = nil - response.each do |message| - page = message.page - next unless page + wiki_page_from_iterator(response) + end - if wiki_page - wiki_page.raw_data << page.raw_data - else - wiki_page = GitalyClient::WikiPage.new(page.to_h) - # All gRPC strings in a response are frozen, so we get - # an unfrozen version here so appending in the else clause below doesn't blow up. - wiki_page.raw_data = wiki_page.raw_data.dup + def get_all_pages + request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request) + pages = [] - version = Gitlab::Git::WikiPageVersion.new( - Gitlab::Git::Commit.decorate(@repository, page.version.commit), - page.version.format - ) - end + loop do + page, version = wiki_page_from_iterator(response) { |message| message.end_of_page } + + break unless page && version + + pages << [page, version] end - [wiki_page, version] + pages end def find_file(name, revision) @@ -133,6 +129,35 @@ module Gitlab private + # If a block is given and the yielded value is true, iteration will be + # stopped early at that point; else the iterator is consumed entirely. + # The iterator is traversed with `next` to allow resuming the iteration. + def wiki_page_from_iterator(iterator) + wiki_page = version = nil + + while message = iterator.next + break if block_given? && yield(message) + + page = message.page + next unless page + + if wiki_page + wiki_page.raw_data << page.raw_data + else + wiki_page = GitalyClient::WikiPage.new(page.to_h) + + version = Gitlab::Git::WikiPageVersion.new( + Gitlab::Git::Commit.decorate(@repository, page.version.commit), + page.version.format + ) + end + end + + [wiki_page, version] + rescue StopIteration + [wiki_page, version] + end + def gitaly_commit_details(commit_details) Gitaly::WikiCommitDetails.new( name: GitalyClient.encode(commit_details.name), diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb index 81739834b41..6da11e6ef08 100644 --- a/lib/gitlab/github_import/parallel_importer.rb +++ b/lib/gitlab/github_import/parallel_importer.rb @@ -11,6 +11,10 @@ module Gitlab true end + def self.imports_repository? + true + end + def initialize(project) @project = project 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/gon_helper.rb b/lib/gitlab/gon_helper.rb index 3a666c2268b..dfcdfc307b6 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -20,7 +20,7 @@ module Gitlab gon.gitlab_url = Gitlab.config.gitlab.url gon.revision = Gitlab::REVISION gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') - gon.sprite_icons = ActionController::Base.helpers.asset_path('icons.svg') + gon.sprite_icons = IconsHelper.sprite_icon_path if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index de9cab80a02..e29dd0d5b0e 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -4,7 +4,6 @@ module Gitlab SAFE_HOOK_ATTRIBUTES = %i[ assignee_id author_id - branch_name closed_at confidential created_at @@ -29,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/importer.rb b/lib/gitlab/import_export/importer.rb index fbdd74788bc..c14646b0611 100644 --- a/lib/gitlab/import_export/importer.rb +++ b/lib/gitlab/import_export/importer.rb @@ -1,6 +1,10 @@ module Gitlab module ImportExport class Importer + def self.imports_repository? + true + end + def initialize(project) @archive_file = project.import_source @current_user = project.creator 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/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/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/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb new file mode 100644 index 00000000000..a2ac9285b56 --- /dev/null +++ b/lib/gitlab/utils/strong_memoize.rb @@ -0,0 +1,31 @@ +module Gitlab + module Utils + module StrongMemoize + # Instead of writing patterns like this: + # + # def trigger_from_token + # return @trigger if defined?(@trigger) + # + # @trigger = Ci::Trigger.find_by_token(params[:token].to_s) + # end + # + # We could write it like: + # + # def trigger_from_token + # strong_memoize(:trigger) do + # Ci::Trigger.find_by_token(params[:token].to_s) + # end + # end + # + def strong_memoize(name) + ivar_name = "@#{name}" + + if instance_variable_defined?(ivar_name) + instance_variable_get(ivar_name) + else + instance_variable_set(ivar_name, yield) + end + end + end + end +end 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/package.json b/package.json index 6315f9eced9..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", @@ -61,8 +62,6 @@ "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", "timeago.js": "^2.0.5", - "ts-loader": "^3.1.1", - "typescript": "^2.6.1", "underscore": "^1.8.3", "url-loader": "^0.5.8", "visibilityjs": "^1.2.4", @@ -76,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", @@ -84,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/bin/qa b/qa/bin/qa index f1704dc54e9..6a772e93cee 100755 --- a/qa/bin/qa +++ b/qa/bin/qa @@ -4,4 +4,4 @@ require_relative '../qa' QA::Scenario .const_get(ARGV.shift) - .launch!(*ARGV) + .launch!(ARGV) @@ -62,6 +62,7 @@ module QA module Main autoload :Entry, 'qa/page/main/entry' + autoload :Login, 'qa/page/main/login' autoload :Menu, 'qa/page/main/menu' end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index b9e199000d6..59cd147e055 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -23,7 +23,7 @@ module QA def password=(pass) @password = pass - @uri.password = pass + @uri.password = CGI.escape(pass) end def use_default_credentials diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index d55326c5262..bdddfb877c5 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -5,7 +5,7 @@ module QA include Scenario::Actable def refresh - visit current_path + visit current_url end end end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb index a9810beeb29..ae6484b4bfe 100644 --- a/qa/qa/page/main/entry.rb +++ b/qa/qa/page/main/entry.rb @@ -2,29 +2,23 @@ module QA module Page module Main class Entry < Page::Base - def initialize - visit('/') + def visit_login_page + visit("#{Runtime::Scenario.gitlab_address}/users/sign_in") + wait_for_instance_to_be_ready + end + + private + def wait_for_instance_to_be_ready # This resolves cold boot / background tasks problems # start = Time.now while Time.now - start < 240 break if page.has_css?('.application', wait: 10) - refresh - end - end - def sign_in_using_credentials - if page.has_content?('Change your password') - fill_in :user_password, with: Runtime::User.password - fill_in :user_password_confirmation, with: Runtime::User.password - click_button 'Change your password' + refresh end - - fill_in :user_login, with: Runtime::User.name - fill_in :user_password, with: Runtime::User.password - click_button 'Sign in' end end end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb new file mode 100644 index 00000000000..8b0111a78a2 --- /dev/null +++ b/qa/qa/page/main/login.rb @@ -0,0 +1,19 @@ +module QA + module Page + module Main + class Login < Page::Base + def sign_in_using_credentials + if page.has_content?('Change your password') + fill_in :user_password, with: Runtime::User.password + fill_in :user_password_confirmation, with: Runtime::User.password + click_button 'Change your password' + end + + fill_in :user_login, with: Runtime::User.name + fill_in :user_password, with: Runtime::User.password + click_button 'Sign in' + end + end + end + end +end diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb index 22496bcc2fc..cf8996cd597 100644 --- a/qa/qa/scenario/bootable.rb +++ b/qa/qa/scenario/bootable.rb @@ -11,6 +11,8 @@ module QA module ClassMethods def launch!(argv) + return self.perform(*argv) unless has_attributes? + arguments = OptionParser.new do |parser| options.to_a.each do |opt| parser.on(opt.arg, opt.desc) do |value| @@ -21,11 +23,7 @@ module QA arguments.parse!(argv) - if has_attributes? - self.perform(**Runtime::Scenario.attributes) - else - self.perform(*argv) - end + self.perform(**Runtime::Scenario.attributes) end private diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index 8f708fe38ab..b9d924651a0 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -7,18 +7,9 @@ module QA class Entrypoint < Template include Bootable - def self.tags(*tags) - @tags = tags - end - - def self.get_tags - @tags - end - def perform(address, *files) - Specs::Config.perform do |specs| - specs.address = address - end + Specs::Config.act { configure_capybara! } + Runtime::Scenario.define(:gitlab_address, address) ## # Perform before hooks, which are different for CE and EE @@ -26,13 +17,19 @@ module QA Runtime::Release.perform_before_hooks Specs::Runner.perform do |specs| - specs.rspec( - tty: true, - tags: self.class.get_tags, - files: files.any? ? files : 'qa/specs/features' - ) + specs.tty = true + specs.tags = self.class.get_tags + specs.files = files.any? ? files : 'qa/specs/features' end end + + def self.tags(*tags) + @tags = tags + end + + def self.get_tags + @tags + end end end end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 79c681168cc..bce7923e52d 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -9,15 +9,9 @@ require 'selenium-webdriver' module QA module Specs class Config < Scenario::Template - attr_writer :address - - def initialize - @address = ENV['GITLAB_URL'] - end + include Scenario::Actable def perform - raise 'Please configure GitLab address!' unless @address - configure_rspec! configure_capybara! end @@ -56,10 +50,9 @@ module QA end Capybara.configure do |config| - config.app_host = @address config.default_driver = :chrome config.javascript_driver = :chrome - config.default_max_wait_time = 4 + config.default_max_wait_time = 10 # https://github.com/mattheworiordan/capybara-screenshot/issues/164 config.save_path = 'tmp' diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index ba19ce17ee5..b155708c387 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,7 +1,8 @@ module QA feature 'standard root login', :core do scenario 'user logs in using credentials' do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index c4afd83c8e4..853a9a6a4f4 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,7 +1,8 @@ module QA feature 'create a new group', :mattermost do scenario 'creating a group with a mattermost team' do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_groups } Page::Dashboard::Groups.perform do |page| diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index a89a6a3d1cf..92f91cb2725 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,7 +1,8 @@ module QA feature 'logging in to Mattermost', :mattermost do scenario 'can use gitlab oauth' do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } Page::Mattermost::Login.act { sign_in_using_oauth } Page::Mattermost::Main.perform do |page| diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index 27eb22f15a6..aba0c2b4c14 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,7 +1,8 @@ module QA feature 'create a new project', :core do scenario 'user creates a new project' do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |project| project.name = 'awesome-project' diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index 3571173783d..5cc3b3b9c1b 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -9,7 +9,8 @@ module QA end before do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| scenario.name = 'project-with-code' diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 0e691fb0d75..30935dc1e13 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -2,7 +2,8 @@ module QA feature 'push code to repository', :core do context 'with regular account over http' do scenario 'user pushes code to the repository' do - Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Entry.act { visit_login_page } + Page::Main::Login.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| scenario.name = 'project_with_code' diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 2aa18d5d3a1..f98b8f88e9a 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -2,16 +2,22 @@ require 'rspec/core' module QA module Specs - class Runner - include Scenario::Actable + class Runner < Scenario::Template + attr_accessor :tty, :tags, :files - def rspec(tty: false, tags: [], files: ['qa/specs/features']) + def initialize + @tty = false + @tags = [] + @files = ['qa/specs/features'] + end + + def perform args = [] - args << '--tty' if tty - tags.to_a.each do |tag| - args << ['-t', tag.to_s] - end - args << files + args.push('--tty') if tty + tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } + args.push(files) + + Specs::Config.perform RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| abort if status.nonzero? diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/entrypoint_spec.rb index 3fd068b641c..aec79dcea04 100644 --- a/qa/spec/scenario/entrypoint_spec.rb +++ b/qa/spec/scenario/entrypoint_spec.rb @@ -6,31 +6,30 @@ describe QA::Scenario::Entrypoint do end context '#perform' do - let(:config) { spy('Specs::Config') } + let(:arguments) { spy('Runtime::Scenario') } let(:release) { spy('Runtime::Release') } let(:runner) { spy('Specs::Runner') } before do - allow(config).to receive(:perform) { |&block| block.call config } - allow(runner).to receive(:perform) { |&block| block.call runner } - - stub_const('QA::Specs::Config', config) stub_const('QA::Runtime::Release', release) + stub_const('QA::Runtime::Scenario', arguments) stub_const('QA::Specs::Runner', runner) + + allow(runner).to receive(:perform).and_yield(runner) end - it 'should set address' do + it 'sets an address of the subject' do subject.perform("hello") - expect(config).to have_received(:address=).with("hello") + expect(arguments).to have_received(:define) + .with(:gitlab_address, "hello") end context 'no paths' do it 'should call runner with default arguments' do subject.perform("test") - expect(runner).to have_received(:rspec) - .with(hash_including(files: 'qa/specs/features')) + expect(runner).to have_received(:files=).with('qa/specs/features') end end @@ -38,8 +37,7 @@ describe QA::Scenario::Entrypoint do it 'should call runner with paths' do subject.perform('test', 'path1', 'path2') - expect(runner).to have_received(:rspec) - .with(hash_including(files: %w(path1 path2))) + expect(runner).to have_received(:files=).with(%w[path1 path2]) 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/create_mysql_user.sh b/scripts/create_mysql_user.sh new file mode 100644 index 00000000000..28f6cfb50ae --- /dev/null +++ b/scripts/create_mysql_user.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +mysql --user=root --host=mysql <<EOF +CREATE DATABASE IF NOT EXISTS gitlabhq_test; +CREATE USER IF NOT EXISTS 'gitlab'@'%'; +GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%'; +FLUSH PRIVILEGES; +EOF diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh new file mode 100644 index 00000000000..8a744df3226 --- /dev/null +++ b/scripts/create_postgres_user.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +psql -h postgres -U postgres postgres <<EOF +DROP DATABASE IF EXISTS gitlabhq_test; +CREATE DATABASE gitlabhq_test; +CREATE USER gitlab; +GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab; +EOF diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 7abadef5e89..36bcf087cd9 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,6 +1,7 @@ . scripts/utils.sh export SETUP_DB=${SETUP_DB:-true} +export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB} export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true} export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet" @@ -26,6 +27,9 @@ fi cp config/database.yml.$GITLAB_DATABASE config/database.yml +# Set user to a non-superuser to ensure we test permissions +sed -i 's/username: root/username: gitlab/g' config/database.yml + if [ "$GITLAB_DATABASE" = 'postgresql' ]; then sed -i 's/localhost/postgres/g' config/database.yml else # Assume it's mysql @@ -44,6 +48,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml cp config/redis.shared_state.yml.example config/redis.shared_state.yml sed -i 's/localhost/redis/g' config/redis.shared_state.yml +# Some tasks (e.g. db:seed_fu) need to have a properly-configured database +# user but not necessarily a full schema loaded +if [ "$CREATE_DB_USER" != "false" ]; then + if [ "$GITLAB_DATABASE" = 'postgresql' ]; then + . scripts/create_postgres_user.sh + else + . scripts/create_mysql_user.sh + fi +fi + if [ "$SETUP_DB" != "false" ]; then bundle exec rake db:drop db:create db:schema:load db:migrate 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/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index cd3bf785d34..d7825364ed5 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -12,12 +12,14 @@ describe IssuableCollections do controller = klass.new - allow(controller).to receive(:params).and_return(state: 'opened') + allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params)) controller end describe '#page_count_for_relation' do + let(:params) { { state: 'opened' } } + it 'returns the number of pages' do relation = double(:relation, limit_value: 20) pages = controller.send(:page_count_for_relation, relation, 28) @@ -25,4 +27,55 @@ describe IssuableCollections do expect(pages).to eq(2) end end + + describe '#filter_params' do + let(:params) do + { + assignee_id: '1', + assignee_username: 'user1', + author_id: '2', + author_username: 'user2', + authorized_only: 'true', + due_date: '2017-01-01', + group_id: '3', + iids: '4', + label_name: 'foo', + milestone_title: 'bar', + my_reaction_emoji: 'thumbsup', + non_archived: 'true', + project_id: '5', + scope: 'all', + search: 'baz', + sort: 'priority', + state: 'opened', + invalid_param: 'invalid_param' + } + end + + it 'filters params' do + allow(controller).to receive(:cookies).and_return({}) + + filtered_params = controller.send(:filter_params) + + expect(filtered_params).to eq({ + 'assignee_id' => '1', + 'assignee_username' => 'user1', + 'author_id' => '2', + 'author_username' => 'user2', + 'authorized_only' => 'true', + 'due_date' => '2017-01-01', + 'group_id' => '3', + 'iids' => '4', + 'label_name' => 'foo', + 'milestone_title' => 'bar', + 'my_reaction_emoji' => 'thumbsup', + 'non_archived' => 'true', + 'project_id' => '5', + 'scope' => 'all', + 'search' => 'baz', + 'sort' => 'priority', + 'state' => 'opened' + }) + end + end end 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/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index f9688949a19..7490f8fefce 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -371,8 +371,10 @@ describe Projects::JobsController do end describe 'POST erase' do + let(:role) { :master } + before do - project.add_developer(user) + project.team << [user, role] sign_in(user) post_erase @@ -404,6 +406,27 @@ describe Projects::JobsController do end end + context 'when user is developer' do + let(:role) { :developer } + let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline, user: triggered_by) } + + context 'when triggered by same user' do + let(:triggered_by) { user } + + it 'has successful status' do + expect(response).to have_gitlab_http_status(:found) + end + end + + context 'when triggered by different user' do + let(:triggered_by) { create(:user) } + + it 'does not have successful status' do + expect(response).not_to have_gitlab_http_status(:found) + end + end + end + def post_erase post :erase, namespace_id: project.namespace, project_id: project, 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/commits_spec.rb b/spec/features/commits_spec.rb index 479fb713297..b163ca8dc75 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 diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb index 9039b283393..da1e17225db 100644 --- a/spec/features/groups/members/manage_members.rb +++ b/spec/features/groups/members/manage_members.rb @@ -44,7 +44,11 @@ feature 'Groups > Members > Manage members' do visit group_group_members_path(group) - find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click + accept_confirm do + find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click + end + + wait_for_requests expect(page).not_to have_content(user2.name) expect(group.users).not_to include(user2) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index b8a66245153..95d637265e0 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -218,18 +218,18 @@ feature 'GFM autocomplete', :js do user_item = find('.atwho-view li', text: user.username) expect(user_item).to have_content(user.username) end + end - def expect_to_wrap(should_wrap, item, note, value) - expect(item).to have_content(value) - expect(item).not_to have_content("\"#{value}\"") + def expect_to_wrap(should_wrap, item, note, value) + expect(item).to have_content(value) + expect(item).not_to have_content("\"#{value}\"") - item.click + item.click - if should_wrap - expect(note.value).to include("\"#{value}\"") - else - expect(note.value).not_to include("\"#{value}\"") - end + if should_wrap + expect(note.value).to include("\"#{value}\"") + else + expect(note.value).not_to include("\"#{value}\"") end end end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 6fbee0ebcb5..4224a8fe5d4 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' feature 'Issue Detail', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project, author: user) } + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, author: user) } context 'when user displays the issue' do before do @@ -27,6 +27,7 @@ feature 'Issue Detail', :js do click_link 'Edit' fill_in 'issuable-title', with: 'issue title' click_button 'Save' + wait_for_requests Users::DestroyService.new(user).execute(user) diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 5402d61da54..db5ce2d11a8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' end + it 'allows filtering multiple dropdowns' do + visit project_new_merge_request_path(project) + + first('.js-source-branch').click + + input = find('.dropdown-source-branch .dropdown-input-field') + input.click + input.send_keys('orphaned-branch') + + find('.dropdown-source-branch .dropdown-content li', match: :first) + source_items = all('.dropdown-source-branch .dropdown-content li') + + expect(source_items.count).to eq(1) + + first('.js-target-branch').click + + find('.dropdown-target-branch .dropdown-content li', match: :first) + target_items = all('.dropdown-target-branch .dropdown-content li') + + expect(target_items.count).to be > 1 + end + context 'when target project cannot be viewed by the current user' do it 'does not leak the private project name & namespace' do private_project = create(:project, :private, :repository) diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 6c9dc67ad74..27efc32c95b 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -65,4 +65,33 @@ feature 'Milestone' do expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.') end end + + feature 'Open a milestone' do + scenario 'shows total issue time spent correctly when no time has been logged' do + milestone = create(:milestone, project: project, title: 8.7) + + visit project_milestone_path(project, milestone) + + page.within('.block.time_spent') do + expect(page).to have_content 'No time spent' + expect(page).to have_content 'None' + end + end + + scenario 'shows total issue time spent' do + milestone = create(:milestone, project: project, title: 8.7) + issue1 = create(:issue, project: project, milestone: milestone) + issue2 = create(:issue, project: project, milestone: milestone) + issue1.spend_time(duration: 3600, user: user) + issue1.save! + issue2.spend_time(duration: 7200, user: user) + issue2.save! + + visit project_milestone_path(project, milestone) + + page.within('.block.time_spent') do + expect(page).to have_content '3h' + end + end + end end diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index 237c059e595..65b11a1d9e7 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -55,6 +55,22 @@ feature 'Project members list' do end end + scenario 'remove user from project', :js do + other_user = create(:user) + project.add_developer(other_user) + + visit_members_page + + accept_confirm do + find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click + end + + wait_for_requests + + expect(page).not_to have_content(other_user.name) + expect(project.users).not_to include(other_user) + end + scenario 'invite user to project', :js do visit_members_page diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index fc689bbb486..50f8f13d261 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -56,31 +56,37 @@ describe 'Pipelines', :js do end it 'shows a tab for All pipelines and count' do - expect(page.find('.js-pipelines-tab-all a').text).to include('All') + expect(page.find('.js-pipelines-tab-all').text).to include('All') expect(page.find('.js-pipelines-tab-all .badge').text).to include('1') end it 'shows a tab for Pending pipelines and count' do - expect(page.find('.js-pipelines-tab-pending a').text).to include('Pending') + expect(page.find('.js-pipelines-tab-pending').text).to include('Pending') expect(page.find('.js-pipelines-tab-pending .badge').text).to include('0') end it 'shows a tab for Running pipelines and count' do - expect(page.find('.js-pipelines-tab-running a').text).to include('Running') + expect(page.find('.js-pipelines-tab-running').text).to include('Running') expect(page.find('.js-pipelines-tab-running .badge').text).to include('1') end it 'shows a tab for Finished pipelines and count' do - expect(page.find('.js-pipelines-tab-finished a').text).to include('Finished') + expect(page.find('.js-pipelines-tab-finished').text).to include('Finished') expect(page.find('.js-pipelines-tab-finished .badge').text).to include('0') end it 'shows a tab for Branches' do - expect(page.find('.js-pipelines-tab-branches a').text).to include('Branches') + expect(page.find('.js-pipelines-tab-branches').text).to include('Branches') end it 'shows a tab for Tags' do - expect(page.find('.js-pipelines-tab-tags a').text).to include('Tags') + expect(page.find('.js-pipelines-tab-tags').text).to include('Tags') + end + + it 'updates content when tab is clicked' do + page.find('.js-pipelines-tab-pending').click + wait_for_requests + expect(page).to have_content('No pipelines to show.') end end @@ -396,6 +402,14 @@ describe 'Pipelines', :js do expect(page).to have_selector('.gl-pagination .page', count: 2) end + + it 'should show updated content' do + visit project_pipelines_path(project) + wait_for_requests + page.find('.js-next-button a').click + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end end end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 15a5cd9990b..a3ea778d401 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -144,7 +144,10 @@ describe 'Edit Project Settings' do specify 'the project is accessible via the new path' do transfer_project(project, group) new_path = namespace_project_path(group, project) + visit new_path + wait_for_requests + expect(current_path).to eq(new_path) expect(find('.breadcrumbs')).to have_content(project.name) end @@ -153,7 +156,10 @@ describe 'Edit Project Settings' do old_path = project_path(project) transfer_project(project, group) new_path = namespace_project_path(group, project) + visit old_path + wait_for_requests + expect(current_path).to eq(new_path) expect(find('.breadcrumbs')).to have_content(project.name) end diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb index 4a152572502..f95469ad070 100644 --- a/spec/features/projects/user_creates_project_spec.rb +++ b/spec/features/projects/user_creates_project_spec.rb @@ -6,10 +6,11 @@ feature 'User creates a project', :js do before do sign_in(user) create(:personal_key, user: user) - visit(new_project_path) end it 'creates a new project' do + visit(new_project_path) + fill_in(:project_path, with: 'Empty') page.within('#content-body') do @@ -24,4 +25,32 @@ feature 'User creates a project', :js do expect(page).to have_content('git remote') expect(page).to have_content(project.url_to_repo) end + + context 'in a subgroup they do not own', :nested_groups do + let(:parent) { create(:group) } + let!(:subgroup) { create(:group, parent: parent) } + + before do + parent.add_owner(user) + end + + it 'creates a new project' do + visit(new_project_path) + + fill_in :project_path, with: 'a-subgroup-project' + + page.find('.js-select-namespace').click + page.find("div[role='option']", text: subgroup.full_path).click + + page.within('#content-body') do + click_button('Create project') + end + + expect(page).to have_content("Project 'a-subgroup-project' was successfully created") + + project = Project.last + + expect(project.namespace).to eq(subgroup) + end + end end diff --git a/spec/features/projects/user_transfers_a_project_spec.rb b/spec/features/projects/user_transfers_a_project_spec.rb new file mode 100644 index 00000000000..78f72b644ff --- /dev/null +++ b/spec/features/projects/user_transfers_a_project_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +feature 'User transfers a project', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, namespace: user.namespace) } + + before do + sign_in user + end + + def transfer_project(project, group) + visit edit_project_path(project) + + page.within('.js-project-transfer-form') do + page.find('.select2-container').click + end + + page.find("div[role='option']", text: group.full_name).click + + click_button('Transfer project') + + fill_in 'confirm_name_input', with: project.name + + click_button 'Confirm' + + wait_for_requests + end + + it 'allows transferring a project to a subgroup of a namespace' do + group = create(:group) + group.add_owner(user) + + transfer_project(project, group) + + expect(project.reload.namespace).to eq(group) + end + + context 'when nested groups are available', :nested_groups do + it 'allows transferring a project to a subgroup' do + parent = create(:group) + parent.add_owner(user) + subgroup = create(:group, parent: parent) + + transfer_project(project, subgroup) + + expect(project.reload.namespace).to eq(subgroup) + end + end +end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb index 684af74d750..dcf9111776e 100644 --- a/spec/finders/autocomplete_users_finder_spec.rb +++ b/spec/finders/autocomplete_users_finder_spec.rb @@ -42,6 +42,21 @@ describe AutocompleteUsersFinder do it { is_expected.to match_array([user1]) } end + context 'when passed a subgroup', :nested_groups do + let(:grandparent) { create(:group, :public) } + let(:parent) { create(:group, :public, parent: grandparent) } + let(:child) { create(:group, :public, parent: parent) } + let(:group) { parent } + + let!(:grandparent_user) { create(:group_member, :developer, group: grandparent).user } + let!(:parent_user) { create(:group_member, :developer, group: parent).user } + let!(:child_user) { create(:group_member, :developer, group: child).user } + + it 'includes users from parent groups as well' do + expect(subject).to match_array([grandparent_user, parent_user]) + end + end + it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) } context 'when filtered by search' do diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json new file mode 100644 index 00000000000..4ba6422406c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "properties": { + "domain": { "type": "string" }, + "url": { "type": "uri" }, + "certificate_expiration": { + "type": "object", + "properties": { + "expired": { "type": "boolean" }, + "expiration": { "type": "string" } + }, + "required": ["expired", "expiration"], + "additionalProperties": false + } + }, + "required": ["domain", "url"], + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json new file mode 100644 index 00000000000..08db8d47050 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "domain": { "type": "string" }, + "url": { "type": "uri" }, + "certificate": { + "type": "object", + "properties": { + "subject": { "type": "string" }, + "expired": { "type": "boolean" }, + "certificate": { "type": "string" }, + "certificate_text": { "type": "string" } + }, + "required": ["subject", "expired"], + "additionalProperties": false + } + }, + "required": ["domain", "url"], + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json new file mode 100644 index 00000000000..c7d86de7d8e --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain_basics.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "pages_domain/basic.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json index 0de1d0f1228..7c27218dc5a 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json @@ -1,23 +1,4 @@ { "type": "array", - "items": { - "type": "object", - "properties": { - "domain": { "type": "string" }, - "url": { "type": "uri" }, - "certificate": { - "type": "object", - "properties": { - "subject": { "type": "string" }, - "expired": { "type": "boolean" }, - "certificate": { "type": "string" }, - "certificate_text": { "type": "string" } - }, - "required": ["subject", "expired"], - "additionalProperties": false - } - }, - "required": ["domain", "url"], - "additionalProperties": false - } + "items": { "$ref": "pages_domain/detail.json" } } diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 3d79dac284f..2f23ed55d99 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe IconsHelper do + let(:icons_path) { ActionController::Base.helpers.image_path("icons.svg") } + describe 'icon' do it 'returns aria-hidden by default' do star = icon('star') @@ -16,22 +18,42 @@ describe IconsHelper do end end + describe 'sprite_icon_path' do + it 'returns relative path' do + expect(sprite_icon_path) + .to eq icons_path + end + + context 'when an asset_host is set in the config it will return an absolute local URL' do + let(:asset_host) { 'http://assets' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + end + + it 'returns an absolute URL on that asset host' do + expect(sprite_icon_path) + .to eq ActionController::Base.helpers.image_path("icons.svg", host: Gitlab.config.gitlab.url) + end + end + end + describe 'sprite_icon' do icon_name = 'clock' it 'returns svg icon html' do expect(sprite_icon(icon_name).to_s) - .to eq "<svg><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + .to eq "<svg><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" end it 'returns svg icon html + size classes' do expect(sprite_icon(icon_name, size: 72).to_s) - .to eq "<svg class=\"s72\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + .to eq "<svg class=\"s72\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" end it 'returns svg icon html + size classes + additional class' do expect(sprite_icon(icon_name, size: 72, css_class: 'icon-danger').to_s) - .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"/images/icons.svg##{icon_name}\"></use></svg>" + .to eq "<svg class=\"s72 icon-danger\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>" end end diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb index 8365b3f5538..460d3b6a7e4 100644 --- a/spec/helpers/namespaces_helper_spec.rb +++ b/spec/helpers/namespaces_helper_spec.rb @@ -29,5 +29,30 @@ describe NamespacesHelper do expect(options).not_to include(admin_group.name) expect(options).to include(user_group.name) end + + context 'when nested groups are available', :nested_groups do + it 'includes groups nested in groups the user can administer' do + allow(helper).to receive(:current_user).and_return(user) + child_group = create(:group, :private, parent: user_group) + + options = helper.namespaces_options + + expect(options).to include(child_group.name) + end + + it 'orders the groups correctly' do + allow(helper).to receive(:current_user).and_return(user) + child_group = create(:group, :private, parent: user_group) + other_child = create(:group, :private, parent: user_group) + sub_child = create(:group, :private, parent: child_group) + + expect(helper).to receive(:options_for_group) + .with([user_group, child_group, sub_child, other_child], anything) + .and_call_original + allow(helper).to receive(:options_for_group).and_call_original + + helper.namespaces_options + end + end end end 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/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/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml index 97b0c25c923..85ee61f0b54 100644 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ b/spec/javascripts/fixtures/pipelines.html.haml @@ -1,16 +1,10 @@ %div #pipelines-list-vue{ data: { endpoint: 'foo', - "css-class" => 'foo', "help-page-path" => 'foo', + "help-auto-devops-path" => 'foo', "empty-state-svg-path" => 'foo', "error-state-svg-path" => 'foo', "new-pipeline-path" => 'foo', "can-create-pipeline" => 'true', - "all-path" => 'foo', - "pending-path" => 'foo', - "running-path" => 'foo', - "finished-path" => 'foo', - "branches-path" => 'foo', - "tags-path" => 'foo', "has-ci" => 'foo', "ci-lint-path" => 'foo' } } diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index ad0c7264616..6f357306ec7 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -67,6 +67,28 @@ describe('GfmAutoComplete', function () { }); }); + describe('DefaultOptions.beforeInsert', () => { + const beforeInsert = (context, value) => ( + gfmAutoCompleteCallbacks.beforeInsert.call(context, value) + ); + + const atwhoInstance = { setting: { skipSpecialCharacterTest: false } }; + + it('should not quote if value only contains alphanumeric charecters', () => { + expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1'); + expect(beforeInsert(atwhoInstance, '~label1')).toBe('~label1'); + }); + + it('should quote if value contains any non-alphanumeric characters', () => { + expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"'); + expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"'); + }); + + it('should quote integer labels', () => { + expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"'); + }); + }); + describe('DefaultOptions.matcher', function () { const defaultMatcher = (context, flag, subtext) => ( gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) 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/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a5298be5669..6dad5d6b6bd 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -183,6 +183,36 @@ describe('common_utils', () => { }); }); + describe('historyPushState', () => { + afterEach(() => { + window.history.replaceState({}, null, null); + }); + + it('should call pushState with the correct path', () => { + spyOn(window.history, 'pushState'); + + commonUtils.historyPushState('newpath?page=2'); + + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2'); + }); + }); + + describe('parseQueryStringIntoObject', () => { + it('should return object with query parameters', () => { + expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ scope: 'all', page: '2' }); + expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); + expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); + }); + }); + + describe('buildUrlWithCurrentLocation', () => { + it('should build an url with current location and given parameters', () => { + expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); + expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual(`${window.location.pathname}?page=2`); + }); + }); + describe('getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js index 83c92deccdc..fcf27f6805f 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/javascripts/lib/utils/number_utility_spec.js @@ -1,4 +1,4 @@ -import { formatRelevantDigits, bytesToKiB, bytesToMiB } from '~/lib/utils/number_utils'; +import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils'; describe('Number Utils', () => { describe('formatRelevantDigits', () => { @@ -52,4 +52,29 @@ describe('Number Utils', () => { expect(bytesToMiB(1000000)).toEqual(0.95367431640625); }); }); + + describe('bytesToGiB', () => { + it('calculates GiB for the given bytes', () => { + expect(bytesToGiB(1073741824)).toEqual(1); + expect(bytesToGiB(10737418240)).toEqual(10); + }); + }); + + describe('numberToHumanSize', () => { + it('should return bytes', () => { + expect(numberToHumanSize(654)).toEqual('654 bytes'); + }); + + it('should return KiB', () => { + expect(numberToHumanSize(1079)).toEqual('1.05 KiB'); + }); + + it('should return MiB', () => { + expect(numberToHumanSize(10485764)).toEqual('10.00 MiB'); + }); + + it('should return GiB', () => { + expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB'); + }); + }); }); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 2aa7011ca51..9b8f68f1676 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -155,7 +155,7 @@ describe('Poll', () => { successCallback: () => { Polling.stop(); setTimeout(() => { - Polling.restart(); + Polling.restart({ data: { page: 4 } }); }, 0); }, errorCallback: callbacks.error, @@ -170,10 +170,10 @@ describe('Poll', () => { Polling.stop(); expect(service.fetch.calls.count()).toEqual(2); - expect(service.fetch).toHaveBeenCalledWith({ page: 1 }); + expect(service.fetch).toHaveBeenCalledWith({ page: 4 }); expect(Polling.stop).toHaveBeenCalled(); expect(Polling.restart).toHaveBeenCalled(); - + expect(Polling.options.data).toEqual({ page: 4 }); done(); }); }); diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js new file mode 100644 index 00000000000..a95a7e2a5be --- /dev/null +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -0,0 +1,62 @@ +import textUtils from '~/lib/utils/text_markdown'; + +describe('init markdown', () => { + let textArea; + + beforeAll(() => { + textArea = document.createElement('textarea'); + document.querySelector('body').appendChild(textArea); + textArea.focus(); + }); + + afterAll(() => { + textArea.parentNode.removeChild(textArea); + }); + + describe('without selection', () => { + it('inserts the tag on an empty line', () => { + const initialValue = ''; + + textArea.value = initialValue; + textArea.selectionStart = 0; + textArea.selectionEnd = 0; + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on a new line if the current one is not empty', () => { + const initialValue = 'some text'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}\n* `); + }); + + it('inserts the tag on the same line if the current line only contains spaces', () => { + const initialValue = ' '; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + + it('inserts the tag on the same line if the current line only contains tabs', () => { + const initialValue = '\t\t\t'; + + textArea.value = initialValue; + textArea.setSelectionRange(initialValue.length, initialValue.length); + + textUtils.insertText(textArea, textArea.value, '*', null, '', false); + + expect(textArea.value).toEqual(`${initialValue}* `); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 829b3ef5735..b21bd958f90 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,109 +1,57 @@ -import { highCountTrim } from '~/lib/utils/text_utility'; +import * as textUtils from '~/lib/utils/text_utility'; describe('text_utility', () => { - describe('gl.text.getTextWidth', () => { - it('returns zero width when no text is passed', () => { - expect(gl.text.getTextWidth('')).toBe(0); + describe('addDelimiter', () => { + it('should add a delimiter to the given string', () => { + expect(textUtils.addDelimiter('1234')).toEqual('1,234'); + expect(textUtils.addDelimiter('222222')).toEqual('222,222'); }); - it('returns zero width when no text is passed and font is passed', () => { - expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); - }); - - it('returns width when text is passed', () => { - expect(gl.text.getTextWidth('foo') > 0).toBe(true); - }); - - it('returns bigger width when font is larger', () => { - const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); - const regular = gl.text.getTextWidth('foo', '10px sans-serif'); - expect(largeFont > regular).toBe(true); - }); - }); - - describe('gl.text.pluralize', () => { - it('returns pluralized', () => { - expect(gl.text.pluralize('test', 2)).toBe('tests'); - }); - - it('returns pluralized when count is 0', () => { - expect(gl.text.pluralize('test', 0)).toBe('tests'); - }); - - it('does not return pluralized', () => { - expect(gl.text.pluralize('test', 1)).toBe('test'); + it('should not add a delimiter if string contains no numbers', () => { + expect(textUtils.addDelimiter('aaaa')).toEqual('aaaa'); }); }); describe('highCountTrim', () => { it('returns 99+ for count >= 100', () => { - expect(highCountTrim(105)).toBe('99+'); - expect(highCountTrim(100)).toBe('99+'); + expect(textUtils.highCountTrim(105)).toBe('99+'); + expect(textUtils.highCountTrim(100)).toBe('99+'); }); it('returns exact number for count < 100', () => { - expect(highCountTrim(45)).toBe(45); + expect(textUtils.highCountTrim(45)).toBe(45); }); }); - describe('gl.text.insertText', () => { - let textArea; - - beforeAll(() => { - textArea = document.createElement('textarea'); - document.querySelector('body').appendChild(textArea); - textArea.focus(); + describe('humanize', () => { + it('should remove underscores and uppercase the first letter', () => { + expect(textUtils.humanize('foo_bar')).toEqual('Foo bar'); }); + }); - afterAll(() => { - textArea.parentNode.removeChild(textArea); + describe('pluralize', () => { + it('should pluralize given string', () => { + expect(textUtils.pluralize('test', 2)).toBe('tests'); }); - describe('without selection', () => { - it('inserts the tag on an empty line', () => { - const initialValue = ''; - - textArea.value = initialValue; - textArea.selectionStart = 0; - textArea.selectionEnd = 0; - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); - - it('inserts the tag on a new line if the current one is not empty', () => { - const initialValue = 'some text'; - - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}\n* `); - }); - - it('inserts the tag on the same line if the current line only contains spaces', () => { - const initialValue = ' '; - - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); - - gl.text.insertText(textArea, textArea.value, '*', null, '', false); - - expect(textArea.value).toEqual(`${initialValue}* `); - }); - - it('inserts the tag on the same line if the current line only contains tabs', () => { - const initialValue = '\t\t\t'; + it('should pluralize when count is 0', () => { + expect(textUtils.pluralize('test', 0)).toBe('tests'); + }); - textArea.value = initialValue; - textArea.setSelectionRange(initialValue.length, initialValue.length); + it('should not pluralize when count is 1', () => { + expect(textUtils.pluralize('test', 1)).toBe('test'); + }); + }); - gl.text.insertText(textArea, textArea.value, '*', null, '', false); + describe('dasherize', () => { + it('should replace underscores with dashes', () => { + expect(textUtils.dasherize('foo_bar_foo')).toEqual('foo-bar-foo'); + }); + }); - expect(textArea.value).toEqual(`${initialValue}* `); - }); + describe('slugify', () => { + it('should remove accents and convert to lower case', () => { + expect(textUtils.slugify('João')).toEqual('joão'); }); }); }); 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/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js index 53a88e6322f..f125a2fa189 100644 --- a/spec/javascripts/pipelines/navigation_tabs_spec.js +++ b/spec/javascripts/pipelines/navigation_tabs_spec.js @@ -8,120 +8,48 @@ describe('navigation tabs pipeline component', () => { let data; beforeEach(() => { - data = { - scope: 'all', - count: { - all: 16, - running: 1, - pending: 10, - finished: 0, + data = [ + { + name: 'All', + scope: 'all', + count: 1, + isActive: true, + }, + { + name: 'Pending', + scope: 'pending', + count: 0, + isActive: false, }, - paths: { - allPath: '/gitlab-org/gitlab-ce/pipelines', - pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending', - finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished', - runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running', - branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches', - tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags', + { + name: 'Running', + scope: 'running', + isActive: false, }, - }; + ]; Component = Vue.extend(navigationTabs); + vm = mountComponent(Component, { tabs: data }); }); afterEach(() => { vm.$destroy(); }); - it('should render tabs with correct paths', () => { - vm = mountComponent(Component, data); - - // All - const allTab = vm.$el.querySelector('.js-pipelines-tab-all a'); - expect(allTab.textContent.trim()).toContain('All'); - expect(allTab.getAttribute('href')).toEqual(data.paths.allPath); - - // Pending - const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a'); - expect(pendingTab.textContent.trim()).toContain('Pending'); - expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath); - - // Running - const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a'); - expect(runningTab.textContent.trim()).toContain('Running'); - expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath); - - // Finished - const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a'); - expect(finishedTab.textContent.trim()).toContain('Finished'); - expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath); - - // Branches - const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a'); - expect(branchesTab.textContent.trim()).toContain('Branches'); - - // Tags - const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a'); - expect(tagsTab.textContent.trim()).toContain('Tags'); + it('should render tabs', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(data.length); }); - describe('scope', () => { - it('should render scope provided as active tab', () => { - vm = mountComponent(Component, data); - expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active'); - }); + it('should render active tab', () => { + expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined(); }); - describe('badges', () => { - it('should render provided number', () => { - vm = mountComponent(Component, data); - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(), - ).toContain(data.count.all); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(), - ).toContain(data.count.pending); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(), - ).toContain(data.count.running); - - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(), - ).toContain(data.count.finished); - }); - - it('should not render badge when number is undefined', () => { - vm = mountComponent(Component, { - scope: 'all', - paths: {}, - count: {}, - }); - - // All - expect( - vm.$el.querySelector('.js-totalbuilds-count'), - ).toEqual(null); - - // Pending - expect( - vm.$el.querySelector('.js-pipelines-tab-pending .badge'), - ).toEqual(null); - - // Running - expect( - vm.$el.querySelector('.js-pipelines-tab-running .badge'), - ).toEqual(null); + it('should render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1'); + expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual('0'); + }); - // Finished - expect( - vm.$el.querySelector('.js-pipelines-tab-finished .badge'), - ).toEqual(null); - }); + it('should not render badge', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index c30abb2edb0..ff38bc1974d 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -9,26 +10,33 @@ describe('Pipelines', () => { preloadFixtures(jsonFixtureName); let PipelinesComponent; - let pipeline; + let pipelines; + let component; beforeEach(() => { loadFixtures('static/pipelines.html.raw'); - const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); + afterEach(() => { + component.$destroy(); + }); + describe('successfull request', () => { describe('with pipelines', () => { const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipeline), { + next(request.respondWith(JSON.stringify(pipelines), { status: 200, })); }; beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); }); afterEach(() => { @@ -38,18 +46,71 @@ describe('Pipelines', () => { }); it('should render table', (done) => { - const component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); - setTimeout(() => { expect(component.$el.querySelector('.table-holder')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); + expect( + component.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); done(); }); }); + + it('should render navigation tabs', (done) => { + setTimeout(() => { + expect( + component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + ).toContain('Pending'); + expect( + component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + ).toContain('All'); + expect( + component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + ).toContain('Running'); + expect( + component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + ).toContain('Finished'); + expect( + component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + ).toContain('Branches'); + expect( + component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + ).toContain('Tags'); + done(); + }); + }); + + it('should make an API request when using tabs', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + component.$el.querySelector('.js-pipelines-tab-finished').click(); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + done(); + }); + }); + + describe('with pagination', () => { + it('should make an API request when using pagination', (done) => { + setTimeout(() => { + spyOn(component, 'updateContent'); + // Mock pagination + component.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + Vue.nextTick(() => { + component.$el.querySelector('.js-next-button a').click(); + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + + done(); + }); + }); + }); + }); }); describe('without pipelines', () => { @@ -70,15 +131,14 @@ describe('Pipelines', () => { }); it('should render empty state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, }).$mount(); setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); + expect(component.$el.querySelector('.empty-state')).not.toBe(null); done(); }); }); @@ -103,7 +163,7 @@ describe('Pipelines', () => { }); it('should render error state', (done) => { - const component = new PipelinesComponent({ + component = new PipelinesComponent({ propsData: { store: new Store(), }, @@ -111,9 +171,50 @@ describe('Pipelines', () => { setTimeout(() => { expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - expect(component.$el.querySelector('.realtime-loading')).toBe(null); done(); }); }); }); + + describe('updateContent', () => { + it('should set given parameters', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + component.updateContent({ scope: 'finished', page: '4' }); + + expect(component.page).toEqual('4'); + expect(component.scope).toEqual('finished'); + expect(component.requestData.scope).toEqual('finished'); + expect(component.requestData.page).toEqual('4'); + }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + + spyOn(component, 'updateContent'); + + component.onChangeTab('running'); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + component = mountComponent(PipelinesComponent, { + store: new Store(), + }); + + spyOn(component, 'updateContent'); + + component.onChangePage(4); + + expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + }); + }); }); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js index 376c291c64b..820a44992b4 100644 --- a/spec/javascripts/repo/helpers.js +++ b/spec/javascripts/repo/helpers.js @@ -12,9 +12,4 @@ export const file = (name = 'name', id = name, type = '') => decorateData({ url: 'url', name, path: name, - last_commit: { - id: '123', - message: 'test', - committed_date: new Date().toISOString(), - }, }); diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js new file mode 100644 index 00000000000..af9d6835a67 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/branch_spec.js @@ -0,0 +1,38 @@ +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { resetStore } from '../../helpers'; + +describe('Multi-file store branch actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('createNewBranch', () => { + beforeEach(() => { + spyOn(service, 'createBranch').and.returnValue(Promise.resolve({ + json: () => ({ + name: 'testing', + }), + })); + spyOn(history, 'pushState'); + + store.state.project.id = 2; + store.state.currentBranch = 'testing'; + }); + + it('creates new branch', (done) => { + store.dispatch('createNewBranch', 'master') + .then(() => { + expect(store.state.currentBranch).toBe('testing'); + expect(service.createBranch).toHaveBeenCalledWith(2, { + branch: 'master', + ref: 'testing', + }); + expect(history.pushState).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js new file mode 100644 index 00000000000..099c0556e71 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/file_spec.js @@ -0,0 +1,417 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store file actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('closeFile', () => { + let localFile; + let getLastCommitDataSpy; + let oldGetLastCommitData; + + beforeEach(() => { + getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); + oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line + store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line + + localFile = file(); + localFile.active = true; + localFile.opened = true; + localFile.parentTreeUrl = 'parentTreeUrl'; + + store.state.openFiles.push(localFile); + + spyOn(history, 'pushState'); + }); + + afterEach(() => { + store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line + }); + + it('closes open files', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('does not close file if has changed', (done) => { + localFile.changed = true; + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeTruthy(); + expect(localFile.active).toBeTruthy(); + expect(store.state.openFiles.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('does not close file if temp file', (done) => { + localFile.tempFile = true; + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(localFile.opened).toBeTruthy(); + expect(localFile.active).toBeTruthy(); + expect(store.state.openFiles.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('force closes a changed file', (done) => { + localFile.changed = true; + + store.dispatch('closeFile', { file: localFile, force: true }) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('calls pushState when no open files are left', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl'); + + done(); + }).catch(done.fail); + }); + + it('sets next file as active', (done) => { + const f = file(); + store.state.openFiles.push(f); + + expect(f.active).toBeFalsy(); + + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(f.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('calls getLastCommitData', (done) => { + store.dispatch('closeFile', { file: localFile }) + .then(() => { + expect(getLastCommitDataSpy).toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + }); + + describe('setFileActive', () => { + let scrollToTabSpy; + let oldScrollToTab; + + beforeEach(() => { + scrollToTabSpy = jasmine.createSpy('scrollToTab'); + oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line + store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line + }); + + afterEach(() => { + store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line + }); + + it('calls scrollToTab', (done) => { + store.dispatch('setFileActive', file()) + .then(() => { + expect(scrollToTabSpy).toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + + it('sets the file active', (done) => { + const localFile = file(); + + store.dispatch('setFileActive', localFile) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('returns early if file is already active', (done) => { + const localFile = file(); + localFile.active = true; + + store.dispatch('setFileActive', localFile) + .then(() => { + expect(scrollToTabSpy).not.toHaveBeenCalled(); + + done(); + }).catch(done.fail); + }); + + it('sets current active file to not active', (done) => { + const localFile = file(); + localFile.active = true; + store.state.openFiles.push(localFile); + + store.dispatch('setFileActive', file()) + .then(() => { + expect(localFile.active).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('resets location.hash for line highlighting', (done) => { + location.hash = 'test'; + + store.dispatch('setFileActive', file()) + .then(() => { + expect(location.hash).not.toBe('test'); + + done(); + }).catch(done.fail); + }); + }); + + describe('getFileData', () => { + let localFile = file(); + + beforeEach(() => { + spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ + headers: { + 'page-title': 'testing getFileData', + }, + json: () => Promise.resolve({ + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }), + })); + + localFile = file(); + localFile.url = 'getFileDataURL'; + }); + + it('calls the service', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); + + done(); + }).catch(done.fail); + }); + + it('sets the file data', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(localFile.blamePath).toBe('blame_path'); + + done(); + }).catch(done.fail); + }); + + it('sets document title', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(document.title).toBe('testing getFileData'); + + done(); + }).catch(done.fail); + }); + + it('sets the file as active', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('adds the file to open files', (done) => { + store.dispatch('getFileData', localFile) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(localFile.name); + + done(); + }).catch(done.fail); + }); + + it('toggles the file loading', (done) => { + store.dispatch('getFileData', localFile) + .then(() => { + expect(localFile.loading).toBeTruthy(); + + return Vue.nextTick(); + }) + .then(() => { + expect(localFile.loading).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + }); + + describe('getRawFileData', () => { + let tmpFile; + + beforeEach(() => { + spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); + + tmpFile = file(); + }); + + it('calls getRawFileData service method', (done) => { + store.dispatch('getRawFileData', tmpFile) + .then(() => { + expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); + + done(); + }).catch(done.fail); + }); + + it('updates file raw data', (done) => { + store.dispatch('getRawFileData', tmpFile) + .then(() => { + expect(tmpFile.raw).toBe('raw'); + + done(); + }).catch(done.fail); + }); + }); + + describe('changeFileContent', () => { + let tmpFile; + + beforeEach(() => { + tmpFile = file(); + }); + + it('updates file content', (done) => { + store.dispatch('changeFileContent', { + file: tmpFile, + content: 'content', + }) + .then(() => { + expect(tmpFile.content).toBe('content'); + + done(); + }).catch(done.fail); + }); + }); + + describe('createTempFile', () => { + beforeEach(() => { + document.body.innerHTML += '<div class="flash-container"></div>'; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + it('creates temp file', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.tempFile).toBeTruthy(); + expect(store.state.tree.length).toBe(1); + + done(); + }).catch(done.fail); + }); + + it('adds tmp file to open files', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(f.name); + + done(); + }).catch(done.fail); + }); + + it('sets tmp file as active', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.active).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('enters edit mode if file is not base64', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then(() => { + expect(store.state.editMode).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('does not enter edit mode if file is base64', (done) => { + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + base64: true, + }).then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('creates flash message is file already exists', (done) => { + store.state.tree.push(file('test', '1', 'blob')); + + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then(() => { + expect(document.querySelector('.flash-alert')).not.toBeNull(); + + done(); + }).catch(done.fail); + }); + + it('increases level of file', (done) => { + store.state.level = 1; + + store.dispatch('createTempFile', { + tree: store.state, + name: 'test', + }).then((f) => { + expect(f.level).toBe(2); + + done(); + }).catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js new file mode 100644 index 00000000000..393a797c6a3 --- /dev/null +++ b/spec/javascripts/repo/stores/actions/tree_spec.js @@ -0,0 +1,469 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { file, resetStore } from '../../helpers'; + +describe('Multi-file store tree actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('getTreeData', () => { + beforeEach(() => { + spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ + headers: { + 'page-title': 'test', + }, + json: () => Promise.resolve({ + last_commit_path: 'last_commit_path', + parent_tree_url: 'parent_tree_url', + path: '/', + trees: [{ name: 'tree' }], + blobs: [{ name: 'blob' }], + submodules: [{ name: 'submodule' }], + }), + })); + spyOn(history, 'pushState'); + + Object.assign(store.state.endpoints, { + rootEndpoint: 'rootEndpoint', + }); + }); + + it('calls service getTreeData', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint'); + + done(); + }).catch(done.fail); + }); + + it('adds data into tree', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree.length).toBe(3); + expect(store.state.tree[0].type).toBe('tree'); + expect(store.state.tree[1].type).toBe('submodule'); + expect(store.state.tree[2].type).toBe('blob'); + + done(); + }).catch(done.fail); + }); + + it('sets parent tree URL', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.parentTreeUrl).toBe('parent_tree_url'); + + done(); + }).catch(done.fail); + }); + + it('sets last commit path', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.lastCommitPath).toBe('last_commit_path'); + + done(); + }).catch(done.fail); + }); + + it('sets root if not currently at root', (done) => { + store.state.isInitialRoot = false; + + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.isInitialRoot).toBeTruthy(); + expect(store.state.isRoot).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('sets page title', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(document.title).toBe('test'); + + done(); + }).catch(done.fail); + }); + + it('toggles loading', (done) => { + store.dispatch('getTreeData') + .then(() => { + expect(store.state.loading).toBeTruthy(); + + return Vue.nextTick(); + }) + .then(() => { + expect(store.state.loading).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('calls pushState with endpoint', (done) => { + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint'); + + done(); + }).catch(done.fail); + }); + + it('calls getLastCommitData if prevLastCommitPath is not null', (done) => { + const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); + const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line + store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line + store.state.prevLastCommitPath = 'test'; + + store.dispatch('getTreeData') + .then(Vue.nextTick) + .then(() => { + expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state); + + store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + }); + + describe('toggleTreeOpen', () => { + let oldGetTreeData; + let getTreeDataSpy; + let tree; + + beforeEach(() => { + getTreeDataSpy = jasmine.createSpy('getTreeData'); + + oldGetTreeData = store._actions.getTreeData; // eslint-disable-line + store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line + + tree = { + opened: false, + tree: [], + }; + }); + + afterEach(() => { + store._actions.getTreeData = oldGetTreeData; // eslint-disable-line + }); + + it('toggles the tree open', (done) => { + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(tree.opened).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('calls getTreeData if tree is closed', (done) => { + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(getTreeDataSpy).toHaveBeenCalledWith({ + endpoint: 'test', + tree, + }); + expect(store.state.previousUrl).toBe('test'); + + done(); + }).catch(done.fail); + }); + + it('resets entries tree', (done) => { + Object.assign(tree, { + opened: true, + tree: ['a'], + }); + + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(tree.tree.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('pushes new state', (done) => { + spyOn(history, 'pushState'); + Object.assign(tree, { + opened: true, + parentTreeUrl: 'testing', + }); + + store.dispatch('toggleTreeOpen', { + endpoint: 'test', + tree, + }).then(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing'); + + done(); + }).catch(done.fail); + }); + }); + + describe('clickedTreeRow', () => { + describe('tree', () => { + let toggleTreeOpenSpy; + let oldToggleTreeOpen; + + beforeEach(() => { + toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen'); + + oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line + store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line + }); + + afterEach(() => { + store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line + }); + + it('opens tree', (done) => { + const tree = { + url: 'a', + type: 'tree', + }; + + store.dispatch('clickedTreeRow', tree) + .then(() => { + expect(toggleTreeOpenSpy).toHaveBeenCalledWith({ + endpoint: tree.url, + tree, + }); + + done(); + }).catch(done.fail); + }); + }); + + describe('submodule', () => { + let row; + + beforeEach(() => { + spyOn(gl.utils, 'visitUrl'); + + row = { + url: 'submoduleurl', + type: 'submodule', + loading: false, + }; + }); + + it('toggles loading for row', (done) => { + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(row.loading).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('opens submodule URL', (done) => { + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl'); + + done(); + }).catch(done.fail); + }); + }); + + describe('blob', () => { + let row; + + beforeEach(() => { + row = { + type: 'blob', + opened: false, + }; + }); + + it('calls getFileData', (done) => { + const getFileDataSpy = jasmine.createSpy('getFileData'); + const oldGetFileData = store._actions.getFileData; // eslint-disable-line + store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line + + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(getFileDataSpy).toHaveBeenCalledWith(row); + + store._actions.getFileData = oldGetFileData; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + + it('calls setFileActive when file is opened', (done) => { + const setFileActiveSpy = jasmine.createSpy('setFileActive'); + const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line + store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line + + row.opened = true; + + store.dispatch('clickedTreeRow', row) + .then(() => { + expect(setFileActiveSpy).toHaveBeenCalledWith(row); + + store._actions.setFileActive = oldSetFileActive; // eslint-disable-line + + done(); + }).catch(done.fail); + }); + }); + }); + + describe('createTempTree', () => { + it('creates temp tree', (done) => { + store.dispatch('createTempTree', 'test') + .then(() => { + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].name).toBe('test'); + expect(store.state.tree[0].type).toBe('tree'); + + done(); + }).catch(done.fail); + }); + + it('creates .gitkeep file in temp tree', (done) => { + store.dispatch('createTempTree', 'test') + .then(() => { + expect(store.state.tree[0].tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].tree[0].name).toBe('.gitkeep'); + + done(); + }).catch(done.fail); + }); + + it('creates new folder inside another tree', (done) => { + const tree = { + type: 'tree', + name: 'testing', + tree: [], + }; + + store.state.tree.push(tree); + + store.dispatch('createTempTree', 'testing/test') + .then(() => { + expect(store.state.tree[0].name).toBe('testing'); + expect(store.state.tree[0].tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].tree[0].name).toBe('test'); + expect(store.state.tree[0].tree[0].type).toBe('tree'); + + done(); + }).catch(done.fail); + }); + + it('does not create new tree if already exists', (done) => { + const tree = { + type: 'tree', + name: 'testing', + tree: [], + }; + + store.state.tree.push(tree); + + store.dispatch('createTempTree', 'testing/test') + .then(() => { + expect(store.state.tree[0].name).toBe('testing'); + expect(store.state.tree[0].tempFile).toBeUndefined(); + + done(); + }).catch(done.fail); + }); + }); + + describe('getLastCommitData', () => { + beforeEach(() => { + spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ + headers: { + 'more-logs-url': null, + }, + json: () => Promise.resolve([{ + type: 'tree', + file_name: 'testing', + commit: { + message: 'commit message', + authored_date: '123', + }, + }]), + })); + + store.state.tree.push(file('testing', '1', 'tree')); + store.state.lastCommitPath = 'lastcommitpath'; + }); + + it('calls service with lastCommitPath', (done) => { + store.dispatch('getLastCommitData') + .then(() => { + expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); + + done(); + }).catch(done.fail); + }); + + it('updates trees last commit data', (done) => { + store.dispatch('getLastCommitData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree[0].lastCommit.message).toBe('commit message'); + + done(); + }).catch(done.fail); + }); + + it('does not update entry if not found', (done) => { + store.state.tree[0].name = 'a'; + + store.dispatch('getLastCommitData') + .then(Vue.nextTick) + .then(() => { + expect(store.state.tree[0].lastCommit.message).not.toBe('commit message'); + + done(); + }).catch(done.fail); + }); + }); + + describe('updateDirectoryData', () => { + it('adds data into tree', (done) => { + const tree = { + tree: [], + }; + const data = { + trees: [{ name: 'tree' }], + submodules: [{ name: 'submodule' }], + blobs: [{ name: 'blob' }], + }; + + store.dispatch('updateDirectoryData', { + data, + tree, + }).then(() => { + expect(tree.tree[0].name).toBe('tree'); + expect(tree.tree[0].type).toBe('tree'); + expect(tree.tree[1].name).toBe('submodule'); + expect(tree.tree[1].type).toBe('submodule'); + expect(tree.tree[2].name).toBe('blob'); + expect(tree.tree[2].type).toBe('blob'); + + done(); + }).catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js new file mode 100644 index 00000000000..f2a7a698912 --- /dev/null +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -0,0 +1,419 @@ +import Vue from 'vue'; +import store from '~/repo/stores'; +import service from '~/repo/services'; +import { resetStore, file } from '../helpers'; + +describe('Multi-file store actions', () => { + afterEach(() => { + resetStore(store); + }); + + describe('redirectToUrl', () => { + it('calls visitUrl', (done) => { + spyOn(gl.utils, 'visitUrl'); + + store.dispatch('redirectToUrl', 'test') + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('test'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('setInitialData', () => { + it('commits initial data', (done) => { + store.dispatch('setInitialData', { canCommit: true }) + .then(() => { + expect(store.state.canCommit).toBeTruthy(); + done(); + }) + .catch(done.fail); + }); + }); + + describe('closeDiscardPopup', () => { + it('closes the discard popup', (done) => { + store.dispatch('closeDiscardPopup', false) + .then(() => { + expect(store.state.discardPopupOpen).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('discardAllChanges', () => { + beforeEach(() => { + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + }); + }); + + describe('closeAllFiles', () => { + beforeEach(() => { + store.state.openFiles.push(file()); + store.state.openFiles[0].opened = true; + }); + + it('closes all open files', (done) => { + store.dispatch('closeAllFiles') + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('toggleEditMode', () => { + it('toggles edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('toggleEditMode') + .then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('sets preview mode', (done) => { + store.state.currentBlobView = 'repo-editor'; + store.state.editMode = true; + + store.dispatch('toggleEditMode') + .then(Vue.nextTick) + .then(() => { + expect(store.state.currentBlobView).toBe('repo-preview'); + + done(); + }).catch(done.fail); + }); + + it('opens discard popup if there are changed files', (done) => { + store.state.editMode = true; + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + + store.dispatch('toggleEditMode') + .then(() => { + expect(store.state.discardPopupOpen).toBeTruthy(); + + done(); + }).catch(done.fail); + }); + + it('can force closed if there are changed files', (done) => { + store.state.editMode = true; + store.state.openFiles.push(file()); + store.state.openFiles[0].changed = true; + + store.dispatch('toggleEditMode', true) + .then(() => { + expect(store.state.discardPopupOpen).toBeFalsy(); + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('discards file changes', (done) => { + const f = file(); + store.state.editMode = true; + store.state.tree.push(f); + store.state.openFiles.push(f); + f.changed = true; + + store.dispatch('toggleEditMode', true) + .then(Vue.nextTick) + .then(() => { + expect(f.changed).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + }); + + describe('toggleBlobView', () => { + it('sets edit mode view if in edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('toggleBlobView') + .then(() => { + expect(store.state.currentBlobView).toBe('repo-editor'); + + done(); + }) + .catch(done.fail); + }); + + it('sets preview mode view if not in edit mode', (done) => { + store.dispatch('toggleBlobView') + .then(() => { + expect(store.state.currentBlobView).toBe('repo-preview'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('checkCommitStatus', () => { + beforeEach(() => { + store.state.project.id = 2; + store.state.currentBranch = 'master'; + store.state.currentRef = '1'; + }); + + it('calls service', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '123' }, + })); + + store.dispatch('checkCommitStatus') + .then(() => { + expect(service.getBranchData).toHaveBeenCalledWith(2, 'master'); + + done(); + }) + .catch(done.fail); + }); + + it('returns true if current ref does not equal returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '123' }, + })); + + store.dispatch('checkCommitStatus') + .then((val) => { + expect(val).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('returns false if current ref equals returned ID', (done) => { + spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ + commit: { id: '1' }, + })); + + store.dispatch('checkCommitStatus') + .then((val) => { + expect(val).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('commitChanges', () => { + let payload; + + beforeEach(() => { + spyOn(window, 'scrollTo'); + + document.body.innerHTML += '<div class="flash-container"></div>'; + + store.state.project.id = 123; + payload = { + branch: 'master', + }; + }); + + afterEach(() => { + document.querySelector('.flash-container').remove(); + }); + + describe('success', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + id: '123456', + short_id: '123', + message: 'test message', + committed_date: 'date', + stats: { + additions: '1', + deletions: '2', + }, + })); + }); + + it('calls service', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(service.commit).toHaveBeenCalledWith(123, payload); + + done(); + }).catch(done.fail); + }); + + it('shows flash notice', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + const alert = document.querySelector('.flash-container'); + + expect(alert.querySelector('.flash-notice')).not.toBeNull(); + expect(alert.textContent.trim()).toBe( + 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', + ); + + done(); + }).catch(done.fail); + }); + + it('adds commit data to changed files', (done) => { + const changedFile = file(); + const f = file(); + changedFile.changed = true; + + store.state.openFiles.push(changedFile, f); + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(changedFile.lastCommit.message).toBe('test message'); + expect(f.lastCommit.message).not.toBe('test message'); + + done(); + }).catch(done.fail); + }); + + it('toggles edit mode', (done) => { + store.state.editMode = true; + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(store.state.editMode).toBeFalsy(); + + done(); + }).catch(done.fail); + }); + + it('closes all files', (done) => { + store.state.openFiles.push(file()); + store.state.openFiles[0].opened = true; + + store.dispatch('commitChanges', { payload, newMr: false }) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }).catch(done.fail); + }); + + it('scrolls to top of page', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(window.scrollTo).toHaveBeenCalledWith(0, 0); + + done(); + }).catch(done.fail); + }); + + it('updates commit ref', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + expect(store.state.currentRef).toBe('123456'); + + done(); + }).catch(done.fail); + }); + + it('redirects to new merge request page', (done) => { + spyOn(gl.utils, 'visitUrl'); + + store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch='; + + store.dispatch('commitChanges', { payload, newMr: true }) + .then(() => { + expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); + + done(); + }).catch(done.fail); + }); + }); + + describe('failed', () => { + beforeEach(() => { + spyOn(service, 'commit').and.returnValue(Promise.resolve({ + message: 'failed message', + })); + }); + + it('shows failed message', (done) => { + store.dispatch('commitChanges', { payload, newMr: false }) + .then(() => { + const alert = document.querySelector('.flash-container'); + + expect(alert.textContent.trim()).toBe( + 'failed message', + ); + + done(); + }).catch(done.fail); + }); + }); + }); + + describe('createTempEntry', () => { + it('creates a temp tree', (done) => { + store.dispatch('createTempEntry', { + name: 'test', + type: 'tree', + }) + .then(() => { + expect(store.state.tree.length).toBe(1); + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].type).toBe('tree'); + + done(); + }) + .catch(done.fail); + }); + + it('creates temp file', (done) => { + store.dispatch('createTempEntry', { + name: 'test', + type: 'blob', + }) + .then(() => { + expect(store.state.tree.length).toBe(1); + expect(store.state.tree[0].tempFile).toBeTruthy(); + expect(store.state.tree[0].type).toBe('blob'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('popHistoryState', () => { + + }); + + describe('scrollToTab', () => { + it('focuses the current active element', (done) => { + document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>'; + const el = document.querySelector('.repo-tab'); + spyOn(el, 'focus'); + + store.dispatch('scrollToTab') + .then(() => { + setTimeout(() => { + expect(el.focus).toHaveBeenCalled(); + + document.getElementById('tabs').remove(); + + done(); + }); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js new file mode 100644 index 00000000000..a204b2386cd --- /dev/null +++ b/spec/javascripts/repo/stores/getters_spec.js @@ -0,0 +1,119 @@ +import * as getters from '~/repo/stores/getters'; +import state from '~/repo/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('treeList', () => { + it('returns flat tree list', () => { + localState.tree.push(file('1')); + localState.tree[0].tree.push(file('2')); + localState.tree[0].tree[0].tree.push(file('3')); + + const treeList = getters.treeList(localState); + + expect(treeList.length).toBe(3); + expect(treeList[1].name).toBe(localState.tree[0].tree[0].name); + expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name); + }); + }); + + describe('changedFiles', () => { + it('returns a list of changed opened files', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('changed')); + localState.openFiles[1].changed = true; + + const changedFiles = getters.changedFiles(localState); + + expect(changedFiles.length).toBe(1); + expect(changedFiles[0].name).toBe('changed'); + }); + }); + + describe('activeFile', () => { + it('returns the current active file', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + localState.openFiles[1].active = true; + + expect(getters.activeFile(localState).name).toBe('active'); + }); + + it('returns undefined if no active files are found', () => { + localState.openFiles.push(file()); + localState.openFiles.push(file('active')); + + expect(getters.activeFile(localState)).toBeUndefined(); + }); + }); + + describe('activeFileExtension', () => { + it('returns the file extension for the current active file', () => { + localState.openFiles.push(file('active')); + localState.openFiles[0].active = true; + localState.openFiles[0].path = 'test.js'; + + expect(getters.activeFileExtension(localState)).toBe('.js'); + + localState.openFiles[0].path = 'test.es6.js'; + + expect(getters.activeFileExtension(localState)).toBe('.js'); + }); + }); + + describe('isCollapsed', () => { + it('returns true if state has open files', () => { + localState.openFiles.push(file()); + + expect(getters.isCollapsed(localState)).toBeTruthy(); + }); + + it('returns false if state has no open files', () => { + expect(getters.isCollapsed(localState)).toBeFalsy(); + }); + }); + + describe('canEditFile', () => { + beforeEach(() => { + localState.onTopOfBranch = true; + localState.canCommit = true; + + localState.openFiles.push(file()); + localState.openFiles[0].active = true; + }); + + it('returns true if user can commit and has open files', () => { + expect(getters.canEditFile(localState)).toBeTruthy(); + }); + + it('returns false if user can commit and has no open files', () => { + localState.openFiles = []; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user can commit and active file is binary', () => { + localState.openFiles[0].binary = true; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user cant commit', () => { + localState.canCommit = false; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + + it('returns false if user can commit but on a branch', () => { + localState.onTopOfBranch = false; + + expect(getters.canEditFile(localState)).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js new file mode 100644 index 00000000000..3c06794d5e3 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/branch_spec.js @@ -0,0 +1,18 @@ +import mutations from '~/repo/stores/mutations/branch'; +import state from '~/repo/stores/state'; + +describe('Multi-file store branch mutations', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('SET_CURRENT_BRANCH', () => { + it('sets currentBranch', () => { + mutations.SET_CURRENT_BRANCH(localState, 'master'); + + expect(localState.currentBranch).toBe('master'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js new file mode 100644 index 00000000000..2f2835dde1f --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/file_spec.js @@ -0,0 +1,131 @@ +import mutations from '~/repo/stores/mutations/file'; +import state from '~/repo/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store file mutations', () => { + let localState; + let localFile; + + beforeEach(() => { + localState = state(); + localFile = file(); + }); + + describe('SET_FILE_ACTIVE', () => { + it('sets the file active', () => { + mutations.SET_FILE_ACTIVE(localState, { + file: localFile, + active: true, + }); + + expect(localFile.active).toBeTruthy(); + }); + }); + + describe('TOGGLE_FILE_OPEN', () => { + beforeEach(() => { + mutations.TOGGLE_FILE_OPEN(localState, localFile); + }); + + it('adds into opened files', () => { + expect(localFile.opened).toBeTruthy(); + expect(localState.openFiles.length).toBe(1); + }); + + it('removes from opened files', () => { + mutations.TOGGLE_FILE_OPEN(localState, localFile); + + expect(localFile.opened).toBeFalsy(); + expect(localState.openFiles.length).toBe(0); + }); + }); + + describe('SET_FILE_DATA', () => { + it('sets extra file data', () => { + mutations.SET_FILE_DATA(localState, { + data: { + blame_path: 'blame', + commits_path: 'commits', + permalink: 'permalink', + raw_path: 'raw', + binary: true, + html: 'html', + render_error: 'render_error', + }, + file: localFile, + }); + + expect(localFile.blamePath).toBe('blame'); + expect(localFile.commitsPath).toBe('commits'); + expect(localFile.permalink).toBe('permalink'); + expect(localFile.rawPath).toBe('raw'); + expect(localFile.binary).toBeTruthy(); + expect(localFile.html).toBe('html'); + expect(localFile.renderError).toBe('render_error'); + }); + }); + + describe('SET_FILE_RAW_DATA', () => { + it('sets raw data', () => { + mutations.SET_FILE_RAW_DATA(localState, { + file: localFile, + raw: 'testing', + }); + + expect(localFile.raw).toBe('testing'); + }); + }); + + describe('UPDATE_FILE_CONTENT', () => { + beforeEach(() => { + localFile.raw = 'test'; + }); + + it('sets content', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + file: localFile, + content: 'test', + }); + + expect(localFile.content).toBe('test'); + }); + + it('sets changed if content does not match raw', () => { + mutations.UPDATE_FILE_CONTENT(localState, { + file: localFile, + content: 'testing', + }); + + expect(localFile.content).toBe('testing'); + expect(localFile.changed).toBeTruthy(); + }); + }); + + describe('DISCARD_FILE_CHANGES', () => { + beforeEach(() => { + localFile.content = 'test'; + localFile.changed = true; + }); + + it('resets content and changed', () => { + mutations.DISCARD_FILE_CHANGES(localState, localFile); + + expect(localFile.content).toBe(''); + expect(localFile.changed).toBeFalsy(); + }); + }); + + describe('CREATE_TMP_FILE', () => { + it('adds file into parent tree', () => { + const f = file(); + + mutations.CREATE_TMP_FILE(localState, { + file: f, + parent: localFile, + }); + + expect(localFile.tree.length).toBe(1); + expect(localFile.tree[0].name).toBe(f.name); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js new file mode 100644 index 00000000000..1c76cfed9c8 --- /dev/null +++ b/spec/javascripts/repo/stores/mutations/tree_spec.js @@ -0,0 +1,71 @@ +import mutations from '~/repo/stores/mutations/tree'; +import state from '~/repo/stores/state'; +import { file } from '../../helpers'; + +describe('Multi-file store tree mutations', () => { + let localState; + let localTree; + + beforeEach(() => { + localState = state(); + localTree = file(); + }); + + describe('TOGGLE_TREE_OPEN', () => { + it('toggles tree open', () => { + mutations.TOGGLE_TREE_OPEN(localState, localTree); + + expect(localTree.opened).toBeTruthy(); + + mutations.TOGGLE_TREE_OPEN(localState, localTree); + + expect(localTree.opened).toBeFalsy(); + }); + }); + + describe('SET_DIRECTORY_DATA', () => { + const data = [{ + name: 'tree', + }, + { + name: 'submodule', + }, + { + name: 'blob', + }]; + + it('adds directory data', () => { + mutations.SET_DIRECTORY_DATA(localState, { + data, + tree: localState, + }); + + expect(localState.tree.length).toBe(3); + expect(localState.tree[0].name).toBe('tree'); + expect(localState.tree[1].name).toBe('submodule'); + expect(localState.tree[2].name).toBe('blob'); + }); + }); + + describe('SET_PARENT_TREE_URL', () => { + it('sets the parent tree url', () => { + mutations.SET_PARENT_TREE_URL(localState, 'test'); + + expect(localState.parentTreeUrl).toBe('test'); + }); + }); + + describe('CREATE_TMP_TREE', () => { + it('adds tree into parent tree', () => { + const tmpEntry = file(); + + mutations.CREATE_TMP_TREE(localState, { + tmpEntry, + parent: localTree, + }); + + expect(localTree.tree.length).toBe(1); + expect(localTree.tree[0].name).toBe(tmpEntry.name); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js new file mode 100644 index 00000000000..d1c9885e01d --- /dev/null +++ b/spec/javascripts/repo/stores/mutations_spec.js @@ -0,0 +1,117 @@ +import mutations from '~/repo/stores/mutations'; +import state from '~/repo/stores/state'; +import { file } from '../helpers'; + +describe('Multi-file store mutations', () => { + let localState; + let entry; + + beforeEach(() => { + localState = state(); + entry = file(); + }); + + describe('SET_INITIAL_DATA', () => { + it('sets all initial data', () => { + mutations.SET_INITIAL_DATA(localState, { + test: 'test', + }); + + expect(localState.test).toBe('test'); + }); + }); + + describe('SET_PREVIEW_MODE', () => { + it('sets currentBlobView to repo-preview', () => { + mutations.SET_PREVIEW_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-preview'); + + localState.currentBlobView = 'testing'; + + mutations.SET_PREVIEW_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-preview'); + }); + }); + + describe('SET_EDIT_MODE', () => { + it('sets currentBlobView to repo-editor', () => { + mutations.SET_EDIT_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-editor'); + + localState.currentBlobView = 'testing'; + + mutations.SET_EDIT_MODE(localState); + + expect(localState.currentBlobView).toBe('repo-editor'); + }); + }); + + describe('TOGGLE_LOADING', () => { + it('toggles loading of entry', () => { + mutations.TOGGLE_LOADING(localState, entry); + + expect(entry.loading).toBeTruthy(); + + mutations.TOGGLE_LOADING(localState, entry); + + expect(entry.loading).toBeFalsy(); + }); + }); + + describe('TOGGLE_EDIT_MODE', () => { + it('toggles editMode', () => { + mutations.TOGGLE_EDIT_MODE(localState); + + expect(localState.editMode).toBeTruthy(); + + mutations.TOGGLE_EDIT_MODE(localState); + + expect(localState.editMode).toBeFalsy(); + }); + }); + + describe('TOGGLE_DISCARD_POPUP', () => { + it('sets discardPopupOpen', () => { + mutations.TOGGLE_DISCARD_POPUP(localState, true); + + expect(localState.discardPopupOpen).toBeTruthy(); + + mutations.TOGGLE_DISCARD_POPUP(localState, false); + + expect(localState.discardPopupOpen).toBeFalsy(); + }); + }); + + describe('SET_COMMIT_REF', () => { + it('sets currentRef', () => { + mutations.SET_COMMIT_REF(localState, '123'); + + expect(localState.currentRef).toBe('123'); + }); + }); + + describe('SET_ROOT', () => { + it('sets isRoot & initialRoot', () => { + mutations.SET_ROOT(localState, true); + + expect(localState.isRoot).toBeTruthy(); + expect(localState.isInitialRoot).toBeTruthy(); + + mutations.SET_ROOT(localState, false); + + expect(localState.isRoot).toBeFalsy(); + expect(localState.isInitialRoot).toBeFalsy(); + }); + }); + + describe('SET_PREVIOUS_URL', () => { + it('sets previousUrl', () => { + mutations.SET_PREVIOUS_URL(localState, 'testing'); + + expect(localState.previousUrl).toBe('testing'); + }); + }); +}); diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js new file mode 100644 index 00000000000..37287c587d7 --- /dev/null +++ b/spec/javascripts/repo/stores/utils_spec.js @@ -0,0 +1,102 @@ +import * as utils from '~/repo/stores/utils'; + +describe('Multi-file store utils', () => { + describe('setPageTitle', () => { + it('sets the document page title', () => { + utils.setPageTitle('test'); + + expect(document.title).toBe('test'); + }); + }); + + describe('pushState', () => { + it('calls history.pushState', () => { + spyOn(history, 'pushState'); + + utils.pushState('test'); + + expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test'); + }); + }); + + describe('createTemp', () => { + it('creates temp tree', () => { + const tmp = utils.createTemp({ + name: 'test', + path: 'test', + type: 'tree', + level: 0, + changed: false, + content: '', + base64: '', + }); + + expect(tmp.tempFile).toBeTruthy(); + expect(tmp.icon).toBe('fa-folder'); + }); + + it('creates temp file', () => { + const tmp = utils.createTemp({ + name: 'test', + path: 'test', + type: 'blob', + level: 0, + changed: false, + content: '', + base64: '', + }); + + expect(tmp.tempFile).toBeTruthy(); + expect(tmp.icon).toBe('fa-file-text-o'); + }); + }); + + describe('findIndexOfFile', () => { + let state; + + beforeEach(() => { + state = [{ + path: '1', + }, { + path: '2', + }]; + }); + + it('finds in the index of an entry by path', () => { + const index = utils.findIndexOfFile(state, { + path: '2', + }); + + expect(index).toBe(1); + }); + }); + + describe('findEntry', () => { + let state; + + beforeEach(() => { + state = { + tree: [{ + type: 'tree', + name: 'test', + }, { + type: 'blob', + name: 'file', + }], + }; + }); + + it('returns an entry found by name', () => { + const foundEntry = utils.findEntry(state, 'tree', 'test'); + + expect(foundEntry.type).toBe('tree'); + expect(foundEntry.name).toBe('test'); + }); + + it('returns undefined when no entry found', () => { + const foundEntry = utils.findEntry(state, 'blob', 'test'); + + expect(foundEntry).toBeUndefined(); + }); + }); +}); 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_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 8832dd161c7..9e6d0aa472c 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -3,13 +3,7 @@ import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; import eventHub from '~/vue_merge_request_widget/event_hub'; import notify from '~/lib/utils/notify'; import mockData from './mock_data'; - -const createComponent = () => { - delete mrWidgetOptions.el; // Prevent component mounting - gl.mrWidgetData = mockData; - const Component = Vue.extend(mrWidgetOptions); - return new Component(); -}; +import mountComponent from '../helpers/vue_mount_component_helper'; const returnPromise = data => new Promise((resolve) => { resolve({ @@ -22,9 +16,16 @@ const returnPromise = data => new Promise((resolve) => { describe('mrWidgetOptions', () => { let vm; + let MrWidgetOptions; beforeEach(() => { - vm = createComponent(); + // Prevent component mounting + delete mrWidgetOptions.el; + + MrWidgetOptions = Vue.extend(mrWidgetOptions); + vm = mountComponent(MrWidgetOptions, { + mrData: { ...mockData }, + }); }); describe('data', () => { @@ -77,7 +78,7 @@ describe('mrWidgetOptions', () => { }); it('should return true if there is relatedLinks in MR', () => { - vm.mr.relatedLinks = {}; + Vue.set(vm.mr, 'relatedLinks', {}); expect(vm.shouldRenderRelatedLinks).toBeTruthy(); }); }); 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..c1eabdede00 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, { 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/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index 1a4ea2bac48..79d2c071446 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -93,7 +93,14 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat end it 'knows it is finished for this range' do - expect(migration.missing_members?(1, 7)).to be_falsy + expect(migration.missing_members?(1, 8)).to be_falsy + end + + it 'does not miss members for forks of forks for which the root was deleted' do + forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id) + base1.destroy + + expect(migration.missing_members?(7, 10)).to be_falsy end context 'with more forks' do 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/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 74a24a4424b..c2bca816aae 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -165,47 +165,16 @@ describe Gitlab::Checks::ChangeAccess do end context 'LFS integrity check' do - let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } + it 'fails if any LFS blobs are missing' do + allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(true) - before do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block| - lazy_block.call([blob_object.id]) - end - end - - context 'with LFS not enabled' do - it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) - - subject.exec - end + expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/) end - context 'with LFS enabled' do - before do - allow(project).to receive(:lfs_enabled?).and_return(true) - end - - context 'deletion' do - let(:changes) { { oldrev: oldrev, ref: ref } } - - it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + it 'succeeds if LFS objects have already been uploaded' do + allow_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).and_return(false) - subject.exec - end - end - - it 'fails if any LFS blobs are missing' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /LFS objects are missing/) - end - - it 'succeeds if LFS objects have already been uploaded' do - lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) - create(:lfs_objects_project, project: project, lfs_object: lfs_object) - - expect { subject.exec }.not_to raise_error - end + expect { subject.exec }.not_to raise_error end end end diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb new file mode 100644 index 00000000000..17756621221 --- /dev/null +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Gitlab::Checks::LfsIntegrity do + include ProjectForksHelper + let(:project) { create(:project, :repository) } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + + subject { described_class.new(project, newrev) } + + describe '#objects_missing?' do + let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } + + before do + allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block| + lazy_block.call([blob_object.id]) + end + end + + context 'with LFS not enabled' do + it 'skips integrity check' do + expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + + subject.objects_missing? + end + end + + context 'with LFS enabled' do + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + context 'deletion' do + let(:newrev) { nil } + + it 'skips integrity check' do + expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + + expect(subject.objects_missing?).to be_falsey + end + end + + it 'is true if any LFS blobs are missing' do + expect(subject.objects_missing?).to be_truthy + end + + it 'is false if LFS objects have already been uploaded' do + lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) + create(:lfs_objects_project, project: project, lfs_object: lfs_object) + + expect(subject.objects_missing?).to be_falsey + end + end + + context 'for forked project' do + let(:parent_project) { create(:project, :repository) } + let(:project) { fork_project(parent_project, nil, repository: true) } + + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + it 'is true parent project is missing LFS objects' do + expect(subject.objects_missing?).to be_truthy + end + + it 'is false parent project already conatins LFS objects for the fork' do + lfs_object = create(:lfs_object, oid: blob_object.lfs_oid) + create(:lfs_objects_project, project: parent_project, lfs_object: lfs_object) + + expect(subject.objects_missing?).to be_falsey + 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/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/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb new file mode 100644 index 00000000000..0506210887c --- /dev/null +++ b/spec/lib/gitlab/git/remote_repository_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Gitlab::Git::RemoteRepository, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + subject { described_class.new(repository) } + + describe '#empty_repo?' do + using RSpec::Parameterized::TableSyntax + + where(:repository, :result) do + Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false + Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true + end + + with_them do + it { expect(subject.empty_repo?).to eq(result) } + end + end + + describe '#commit_id' do + it 'returns an OID if the revision exists' do + expect(subject.commit_id('v1.0.0')).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + end + + it 'is nil when the revision does not exist' do + expect(subject.commit_id('does-not-exist')).to be_nil + end + end + + describe '#branch_exists?' do + using RSpec::Parameterized::TableSyntax + + where(:branch, :result) do + 'master' | true + 'does-not-exist' | false + end + + with_them do + it { expect(subject.branch_exists?(branch)).to eq(result) } + end + end + + describe '#same_repository?' do + using RSpec::Parameterized::TableSyntax + + where(:other_repository, :result) do + repository | true + Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true + Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false + Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false + Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false + end + + with_them do + it { expect(subject.same_repository?(other_repository)).to eq(result) } + end + end + + describe '#fetch_env' do + let(:remote_repository) { described_class.new(repository) } + + let(:gitaly_client) { double(:gitaly_client) } + let(:address) { 'fake-address' } + let(:token) { 'fake-token' } + + subject { remote_repository.fetch_env } + + before do + allow(remote_repository).to receive(:gitaly_client).and_return(gitaly_client) + + expect(gitaly_client).to receive(:address).with(repository.storage).and_return(address) + expect(gitaly_client).to receive(:token).with(repository.storage).and_return(token) + end + + it { expect(subject).to be_a(Hash) } + it { expect(subject['GITALY_ADDRESS']).to eq(address) } + it { expect(subject['GITALY_TOKEN']).to eq(token) } + it { expect(subject['GITALY_WD']).to eq(Dir.pwd) } + + it 'creates a plausible GIT_SSH_COMMAND' do + git_ssh_command = subject['GIT_SSH_COMMAND'] + + expect(git_ssh_command).to start_with('/') + expect(git_ssh_command).to end_with('/gitaly-ssh upload-pack') + end + + it 'creates a plausible GITALY_PAYLOAD' do + req = Gitaly::SSHUploadPackRequest.decode_json(subject['GITALY_PAYLOAD']) + + expect(remote_repository.gitaly_repository).to eq(req.repository) + end + + context 'when the token is blank' do + let(:token) { '' } + + it { expect(subject.keys).not_to include('GITALY_TOKEN') } + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index ee14b528ec2..e226635971d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -449,7 +449,6 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } after do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end @@ -484,7 +483,6 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } after do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end @@ -544,7 +542,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end end @@ -570,7 +567,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end end @@ -588,7 +584,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end end @@ -634,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 @@ -1122,7 +1130,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end @@ -1169,7 +1176,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end @@ -1419,7 +1425,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end @@ -1537,35 +1542,60 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#fetch_source_branch!' do - let(:local_ref) { 'refs/merge-requests/1/head' } + shared_examples '#fetch_source_branch!' do + let(:local_ref) { 'refs/merge-requests/1/head' } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - context 'when the branch exists' do - let(:source_branch) { 'master' } + after do + ensure_seeds + end - it 'writes the ref' do - expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/) + context 'when the branch exists' do + context 'when the commit does not exist locally' do + let(:source_branch) { 'new-branch-for-fetch-source-branch' } + let(:source_rugged) { source_repository.rugged } + let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } - repository.fetch_source_branch!(repository, source_branch, local_ref) - end + before do + source_rugged.branches.create(source_branch, new_oid) + end - it 'returns true' do - expect(repository.fetch_source_branch!(repository, source_branch, local_ref)).to eq(true) - end - end + it 'writes the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(new_oid) + end + end - context 'when the branch does not exist' do - let(:source_branch) { 'definitely-not-master' } + context 'when the commit exists locally' do + let(:source_branch) { 'master' } + let(:expected_oid) { SeedRepo::LastCommit::ID } - it 'does not write the ref' do - expect(repository).not_to receive(:write_ref) + it 'writes the ref' do + # Sanity check: the commit should already exist + expect(repository.commit(expected_oid)).not_to be_nil - repository.fetch_source_branch!(repository, source_branch, local_ref) + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(expected_oid) + end + end end - it 'returns false' do - expect(repository.fetch_source_branch!(repository, source_branch, local_ref)).to eq(false) + context 'when the branch does not exist' do + let(:source_branch) { 'definitely-not-master' } + + it 'does not write the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) + expect(repository.commit(local_ref)).to be_nil + end end end + + it_behaves_like '#fetch_source_branch!' + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#fetch_source_branch!' + end end describe '#rm_branch' do @@ -1641,7 +1671,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end after do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds end diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb new file mode 100644 index 00000000000..6ad9f5ef766 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::WikiService do + let(:project) { create(:project) } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.disk_path + '.git' } + let(:client) { described_class.new(project.repository) } + let(:commit) { create(:gitaly_commit) } + let(:page_version) { Gitaly::WikiPageVersion.new(format: 'markdown', commit: commit) } + let(:page_info) { { title: 'My Page', raw_data: 'a', version: page_version } } + + describe '#find_page' do + let(:response) do + [ + Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(page_info)), + Gitaly::WikiFindPageResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b')) + ] + end + let(:wiki_page) { subject.first } + let(:wiki_page_version) { subject.last } + + subject { client.find_page(title: 'My Page', version: 'master', dir: '') } + + it 'sends a wiki_find_page message' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_find_page) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([].each) + + subject + end + + it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_find_page) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(response.each) + + expect(wiki_page.title).to eq('My Page') + expect(wiki_page.raw_data).to eq('ab') + expect(wiki_page_version.format).to eq('markdown') + end + end + + describe '#get_all_pages' do + let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } } + let(:response) do + [ + Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_info)), + Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'b')), + Gitaly::WikiGetAllPagesResponse.new(end_of_page: true), + Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(page_2_info)), + Gitaly::WikiGetAllPagesResponse.new(page: Gitaly::WikiPage.new(raw_data: 'd')), + Gitaly::WikiGetAllPagesResponse.new(end_of_page: true) + ] + end + let(:wiki_page_1) { subject[0].first } + let(:wiki_page_1_version) { subject[0].last } + let(:wiki_page_2) { subject[1].first } + let(:wiki_page_2_version) { subject[1].last } + + subject { client.get_all_pages } + + it 'sends a wiki_get_all_pages message' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_get_all_pages) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return([].each) + + subject + end + + it 'concatenates the raw data and returns a pair of WikiPage and WikiPageVersion for each page' do + expect_any_instance_of(Gitaly::WikiService::Stub) + .to receive(:wiki_get_all_pages) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(response.each) + + expect(subject.size).to be(2) + expect(wiki_page_1.title).to eq('My Page') + expect(wiki_page_1.raw_data).to eq('ab') + expect(wiki_page_1_version.format).to eq('markdown') + expect(wiki_page_2.title).to eq('My Page 2') + expect(wiki_page_2.raw_data).to eq('cd') + expect(wiki_page_2_version.format).to eq('markdown') + 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/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index 6c529cdd051..aeacd577d18 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -11,7 +11,6 @@ describe Gitlab::HookData::IssueBuilder do %w[ assignee_id author_id - branch_name closed_at confidential created_at 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/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/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb new file mode 100644 index 00000000000..4a104ab6d97 --- /dev/null +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::Utils::StrongMemoize do + let(:klass) do + struct = Struct.new(:value) do + def method_name + strong_memoize(:method_name) do + trace << value + value + end + end + + def trace + @trace ||= [] + end + end + + struct.include(described_class) + struct + end + + subject(:object) { klass.new(value) } + + shared_examples 'caching the value' do + it 'only calls the block once' do + value0 = object.method_name + value1 = object.method_name + + expect(value0).to eq(value) + expect(value1).to eq(value) + expect(object.trace).to contain_exactly(value) + end + + it 'returns and defines the instance variable for the exact value' do + returned_value = object.method_name + memoized_value = object.instance_variable_get(:@method_name) + + expect(returned_value).to eql(value) + expect(memoized_value).to eql(value) + end + end + + describe '#strong_memoize' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + + it_behaves_like 'caching the value' + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index c832cee965b..f942a22b6d1 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -783,7 +783,25 @@ describe Notify do shared_examples 'an email for a note on a diff discussion' do |model| let(:note) { create(model, author: note_author) } - it "includes diffs with character-level highlighting" do + context 'when note is on image' do + before do + allow_any_instance_of(DiffDiscussion).to receive(:on_image?).and_return(true) + end + + it 'does not include diffs with character-level highlighting' do + is_expected.not_to have_body_text '<span class="p">}</span></span>' + end + + it 'ends the intro with a dot' do + is_expected.to have_body_text "#{note.diff_file.file_path}</a>." + end + end + + it 'ends the intro with a colon' do + is_expected.to have_body_text "#{note.diff_file.file_path}</a>:" + end + + it 'includes diffs with character-level highlighting' do is_expected.to have_body_text '<span class="p">}</span></span>' 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/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 5ed2e1ca99a..1795ee8e9a4 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -270,6 +270,23 @@ describe Ci::Build do end end + describe '#triggered_by?' do + subject { build.triggered_by?(user) } + + context 'when user is owner' do + let(:build) { create(:ci_build, pipeline: pipeline, user: user) } + + it { is_expected.to be_truthy } + end + + context 'when user is not owner' do + let(:another_user) { create(:user) } + let(:build) { create(:ci_build, pipeline: pipeline, user: another_user) } + + it { is_expected.to be_falsy } + end + end + describe '#detailed_status' do it 'returns a detailed status' do expect(build.detailed_status(user)) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2c9e7013b77..b89b0e555d9 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 } 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/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index 66353935427..9048da0c73d 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -186,4 +186,21 @@ describe Milestone, 'Milestoneish' do expect(milestone.elapsed_days).to eq(2) end end + + describe '#total_issue_time_spent' do + it 'calculates total issue time spent' do + closed_issue_1.spend_time(duration: 300, user: author) + closed_issue_1.save! + closed_issue_2.spend_time(duration: 600, user: assignee) + closed_issue_2.save! + + expect(milestone.total_issue_time_spent).to eq(900) + end + end + + describe '#human_total_issue_time_spent' do + it 'returns nil if no time has been spent' do + expect(milestone.human_total_issue_time_spent).to be_nil + end + end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index da972d2d86a..8389d5c5430 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -283,6 +283,12 @@ describe DiffNote do expect(diff_line).to be nil expect(subject).to be_valid end + + it "does not update the position" do + expect(subject).not_to receive(:update_position) + + subject.save + end end it "returns true for on_image?" do 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/issue_spec.rb b/spec/models/issue_spec.rb index bb5033c1628..5f901262598 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -765,22 +765,4 @@ describe Issue do expect(described_class.public_only).to eq([public_issue]) end end - - describe '#update_project_counter_caches?' do - it 'returns true when the state changes' do - subject.state = 'closed' - - expect(subject.update_project_counter_caches?).to eq(true) - end - - it 'returns true when the confidential flag changes' do - subject.confidential = true - - expect(subject.update_project_counter_caches?).to eq(true) - end - - it 'returns false when the state or confidential flag did not change' do - expect(subject.update_project_counter_caches?).to eq(false) - 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/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d022dae3476..d250ad50713 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1772,16 +1772,4 @@ describe MergeRequest do .to change { project.open_merge_requests_count }.from(1).to(0) end end - - describe '#update_project_counter_caches?' do - it 'returns true when the state changes' do - subject.state = 'closed' - - expect(subject.update_project_counter_caches?).to eq(true) - end - - it 'returns false when the state did not change' do - expect(subject.update_project_counter_caches?).to eq(false) - 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/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_spec.rb b/spec/models/project_spec.rb index 4db417c8793..f7f19d464d1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1938,6 +1938,24 @@ describe Project do expect(second_fork.fork_source).to eq(project) end end + + describe '#lfs_storage_project' do + it 'returns self for non-forks' do + expect(project.lfs_storage_project).to eq project + end + + it 'returns the fork network root for forks' do + second_fork = fork_project(forked_project) + + expect(second_fork.lfs_storage_project).to eq project + end + + it 'returns self when fork_source is nil' do + expect(forked_project).to receive(:fork_source).and_return(nil) + + expect(forked_project.lfs_storage_project).to eq forked_project + end + end end describe '#pushes_since_gc' do 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 65dccf9638d..86647ddf6ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -642,16 +642,40 @@ describe User do end describe 'groups' do + let(:user) { create(:user) } + let(:group) { create(:group) } + before do - @user = create :user - @group = create :group - @group.add_owner(@user) + group.add_owner(user) end - it { expect(@user.several_namespaces?).to be_truthy } - it { expect(@user.authorized_groups).to eq([@group]) } - it { expect(@user.owned_groups).to eq([@group]) } - it { expect(@user.namespaces).to match_array([@user.namespace, @group]) } + it { expect(user.several_namespaces?).to be_truthy } + it { expect(user.authorized_groups).to eq([group]) } + it { expect(user.owned_groups).to eq([group]) } + it { expect(user.namespaces).to contain_exactly(user.namespace, group) } + it { expect(user.manageable_namespaces).to contain_exactly(user.namespace, group) } + + context 'with child groups', :nested_groups do + let!(:subgroup) { create(:group, parent: group) } + + describe '#manageable_namespaces' do + it 'includes all the namespaces the user can manage' do + expect(user.manageable_namespaces).to contain_exactly(user.namespace, group, subgroup) + end + end + + describe '#manageable_groups' do + it 'includes all the namespaces the user can manage' do + expect(user.manageable_groups).to contain_exactly(group, subgroup) + end + + it 'does not include duplicates if a membership was added for the subgroup' do + subgroup.add_owner(user) + + expect(user.manageable_groups).to contain_exactly(group, subgroup) + end + end + end end describe 'group multiple owners' do @@ -804,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, @@ -819,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/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 8e1bc3d1543..298a9d16425 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -150,5 +150,82 @@ describe Ci::BuildPolicy do end end end + + describe 'rules for erase build' do + let(:project) { create(:project, :repository) } + let(:build) { create(:ci_build, pipeline: pipeline, ref: 'some-ref', user: owner) } + + context 'when a developer erases a build' do + before do + project.add_developer(user) + end + + context 'when developers can push to the branch' do + before do + create(:protected_branch, :developers_can_push, + name: build.ref, project: project) + end + + context 'when the build was created by the developer' do + let(:owner) { user } + + it { expect(policy).to be_allowed :erase_build } + end + + context 'when the build was created by the other' do + let(:owner) { create(:user) } + + it { expect(policy).to be_disallowed :erase_build } + end + end + + context 'when no one can push or merge to the branch' do + let(:owner) { user } + + before do + create(:protected_branch, :no_one_can_push, :no_one_can_merge, + name: build.ref, project: project) + end + + it { expect(policy).to be_disallowed :erase_build } + end + end + + context 'when a master erases a build' do + before do + project.add_master(user) + end + + context 'when masters can push to the branch' do + before do + create(:protected_branch, :masters_can_push, + name: build.ref, project: project) + end + + context 'when the build was created by the master' do + let(:owner) { user } + + it { expect(policy).to be_allowed :erase_build } + end + + context 'when the build was created by the other' do + let(:owner) { create(:user) } + + it { expect(policy).to be_allowed :erase_build } + end + end + + context 'when no one can push or merge to the branch' do + let(:owner) { user } + + before do + create(:protected_branch, :no_one_can_push, :no_one_can_merge, + name: build.ref, project: project) + end + + it { expect(policy).to be_disallowed :erase_build } + end + end + end end end 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/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 1765907c1b4..2a83213e87a 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -500,7 +500,11 @@ describe API::Jobs do end describe 'POST /projects/:id/jobs/:job_id/erase' do + let(:role) { :master } + before do + project.team << [user, role] + post api("/projects/#{project.id}/jobs/#{job.id}/erase", user) end @@ -529,6 +533,23 @@ describe API::Jobs do expect(response).to have_gitlab_http_status(403) end end + + context 'when a developer erases a build' do + let(:role) { :developer } + let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, user: owner) } + + context 'when the build was created by the developer' do + let(:owner) { user } + + it { expect(response).to have_gitlab_http_status(201) } + end + + context 'when the build was created by the other' do + let(:owner) { create(:user) } + + it { expect(response).to have_gitlab_http_status(403) } + end + end end describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index d13b3a958c9..d412b045e9f 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' describe API::PagesDomains do set(:project) { create(:project) } set(:user) { create(:user) } + set(:admin) { create(:admin) } set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) } set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) } @@ -23,12 +24,49 @@ describe API::PagesDomains do allow(Gitlab.config.pages).to receive(:enabled).and_return(true) end + describe 'GET /pages/domains' do + context 'when pages is disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + it_behaves_like '404 response' do + let(:request) { get api('/pages/domains', admin) } + end + end + + context 'when pages is enabled' do + context 'when authenticated as an admin' do + it 'returns paginated all pages domains' do + get api('/pages/domains', admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain_basics') + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.last).to have_key('domain') + expect(json_response.last).to have_key('certificate_expiration') + expect(json_response.last['certificate_expiration']['expired']).to be true + expect(json_response.first).not_to have_key('certificate_expiration') + end + end + + context 'when authenticated as a non-member' do + it_behaves_like '403 response' do + let(:request) { get api('/pages/domains', user) } + end + end + end + end + describe 'GET /projects/:project_id/pages/domains' do shared_examples_for 'get pages domains' do it 'returns paginated pages domains' do get api(route, user) expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domains') expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) @@ -99,6 +137,7 @@ describe API::PagesDomains do get api(route_domain, user) expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(json_response['domain']).to eq(pages_domain.domain) expect(json_response['url']).to eq(pages_domain.url) expect(json_response['certificate']).to be_nil @@ -108,6 +147,7 @@ describe API::PagesDomains do get api(route_secure_domain, user) expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(json_response['domain']).to eq(pages_domain_secure.domain) expect(json_response['url']).to eq(pages_domain_secure.url) expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject) @@ -118,6 +158,7 @@ describe API::PagesDomains do get api(route_expired_domain, user) expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(json_response['certificate']['expired']).to be true end end @@ -187,6 +228,7 @@ describe API::PagesDomains do pages_domain = PagesDomain.find_by(domain: json_response['domain']) expect(response).to have_gitlab_http_status(201) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain.domain).to eq(params[:domain]) expect(pages_domain.certificate).to be_nil expect(pages_domain.key).to be_nil @@ -197,6 +239,7 @@ describe API::PagesDomains do pages_domain = PagesDomain.find_by(domain: json_response['domain']) expect(response).to have_gitlab_http_status(201) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain.domain).to eq(params_secure[:domain]) expect(pages_domain.certificate).to eq(params_secure[:certificate]) expect(pages_domain.key).to eq(params_secure[:key]) @@ -270,6 +313,7 @@ describe API::PagesDomains do pages_domain_secure.reload expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain_secure.certificate).to be_nil expect(pages_domain_secure.key).to be_nil end @@ -279,6 +323,7 @@ describe API::PagesDomains do pages_domain.reload expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain.certificate).to eq(params_secure[:certificate]) expect(pages_domain.key).to eq(params_secure[:key]) end @@ -288,6 +333,7 @@ describe API::PagesDomains do pages_domain_expired.reload expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain_expired.certificate).to eq(params_secure[:certificate]) expect(pages_domain_expired.key).to eq(params_secure[:key]) end @@ -297,6 +343,7 @@ describe API::PagesDomains do pages_domain_secure.reload expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate]) 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/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index 3f58b7ef384..a73bb456b52 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -408,6 +408,8 @@ describe API::V3::Builds do describe 'POST /projects/:id/builds/:build_id/erase' do before do + project.add_master(user) + post v3_api("/projects/#{project.id}/builds/#{build.id}/erase", user) end 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/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 214adc9960f..0ce41e7c7ee 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -292,6 +292,30 @@ describe Ci::ProcessPipelineService, '#execute' do end end + context 'when there is only one manual action' do + before do + create_build('deploy', stage_idx: 0, when: 'manual', allow_failure: true) + + process_pipeline + end + + it 'skips the pipeline' do + expect(pipeline.reload).to be_skipped + end + + context 'when the action was played' do + before do + play_manual_action('deploy') + end + + it 'queues the action and pipeline' do + expect(all_builds_statuses).to eq(%w[pending]) + + expect(pipeline.reload).to be_pending + end + end + end + context 'when blocking manual actions are defined' do before do create_build('code:test', stage_idx: 0) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index ac196e92601..f86f1ac2443 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -248,6 +248,28 @@ describe MergeRequests::MergeService do expect(merge_request.merge_error).to include(error_message) expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) end + + context "when fast-forward merge is not allowed" do + before do + allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil) + end + + %w(semi-linear ff).each do |merge_method| + it "logs and saves error if merge is #{merge_method} only" do + merge_method = 'rebase_merge' if merge_method == 'semi-linear' + merge_request.project.update(merge_method: merge_method) + error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch' + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request).to be_open + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to include(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + end + end 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 5739386dd0d..af35e17bfa7 100644 --- a/spec/services/milestones/destroy_service_spec.rb +++ b/spec/services/milestones/destroy_service_spec.rb @@ -4,8 +4,8 @@ describe Milestones::DestroyService do let(:user) { create(:user) } 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!(:issue) { create(:issue, 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/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 89fb362cf14..c7e8a39a617 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -1,6 +1,11 @@ RSpec.configure do |config| config.before(:each) do |example| - next if example.metadata[:skip_gitaly_mock] - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) + if example.metadata[:disable_gitaly] + 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 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/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index 558ff9109ec..0fa19ac84bb 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -23,5 +23,17 @@ describe UpdateMergeRequestsWorker do perform end + + context 'when slow' do + before do + stub_const("UpdateMergeRequestsWorker::LOG_TIME_THRESHOLD", -1) + end + + it 'logs debug info' do + expect(Rails.logger).to receive(:info).with(a_string_matching(/\AUpdateMergeRequestsWorker#perform.*project_id=#{project.id},user_id=#{user.id},oldrev=#{oldrev},newrev=#{newrev},ref=#{ref}/)) + + perform + end + end end end diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 3a569cf7c19..00000000000 --- a/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "strict": true, - "module": "es2015", - "moduleResolution": "node" - } -}
\ No newline at end of file diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js deleted file mode 100644 index da37966b28a..00000000000 --- a/vendor/assets/javascripts/latinise.js +++ /dev/null @@ -1,11 +0,0 @@ -// Converting text to basic latin (aka removing accents) -// -// Based on: http://semplicewebsites.com/removing-accents-javascript -// -var Latinise = { - map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"} -}; - -String.prototype.latinise = function() { - return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; }); -}; diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index c79ba5080a3..addf405e4f5 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -32,7 +32,7 @@ proguard/ # Android Studio captures folder captures/ -# Intellij +# IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore index 9bf1537f6ae..ecf66f84291 100644 --- a/vendor/gitignore/Perl.gitignore +++ b/vendor/gitignore/Perl.gitignore @@ -24,7 +24,7 @@ Build.bat # Module::Install inc/ -# ExtUitls::MakeMaker +# ExtUtils::MakeMaker /blib/ /_eumm/ /*.gz diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml index 02cfab3a5b2..36386a19fdc 100644 --- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml @@ -16,7 +16,7 @@ image: "crystallang/crystal:latest" # Cache shards in between builds cache: paths: - - libs + - lib # This is a basic example for a shard or script which doesn't use # services such as redis or postgres diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 9f78059986d..6f6ca5f8b32 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -1,7 +1,11 @@ +"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD" RedCloth,4.3.2,MIT abbrev,1.0.9,ISC +abbrev,1.1.0,ISC accepts,1.3.3,MIT ace-rails-ap,4.1.2,MIT +acorn,3.3.0,MIT +acorn,4.0.13,MIT acorn,5.1.1,MIT acorn-dynamic-import,2.0.2,MIT acorn-jsx,3.0.1,MIT @@ -15,7 +19,9 @@ activesupport,4.2.8,MIT acts-as-taggable-on,4.0.0,MIT addressable,2.5.2,Apache 2.0 after,0.8.2,MIT +ajv,4.11.8,MIT ajv,5.2.2,MIT +ajv-keywords,1.5.1,MIT ajv-keywords,2.1.0,MIT akismet,2.0.0,MIT align-text,0.1.4,MIT @@ -24,10 +30,14 @@ alphanum-sort,1.0.2,MIT amdefine,1.0.1,BSD-3-Clause OR MIT ansi-escapes,1.4.0,MIT ansi-html,0.0.5,"Apache, Version 2.0" +ansi-html,0.0.7,Apache 2.0 ansi-regex,2.1.1,MIT ansi-styles,2.2.1,MIT +ansi-styles,3.2.0,MIT anymatch,1.3.2,ISC append-transform,0.4.0,MIT +aproba,1.1.1,ISC +are-we-there-yet,1.1.4,ISC arel,6.0.4,MIT argparse,1.0.9,MIT arr-diff,2.0.0,MIT @@ -35,6 +45,7 @@ arr-flatten,1.0.1,MIT array-find,1.0.0,MIT array-find-index,1.0.2,MIT array-flatten,1.1.1,MIT +array-flatten,2.1.1,MIT array-slice,0.2.3,MIT array-union,1.0.2,MIT array-uniq,1.0.3,MIT @@ -44,15 +55,24 @@ arrify,1.0.1,MIT asana,0.6.0,MIT asciidoctor,1.5.3,MIT asciidoctor-plantuml,0.0.7,MIT +asn1,0.2.3,MIT asn1.js,4.9.1,MIT assert,1.4.1,MIT +assert-plus,0.2.0,MIT +assert-plus,1.0.0,MIT +async,0.9.2,MIT +async,1.5.2,MIT async,2.4.1,MIT async-each,1.0.1,MIT +asynckit,0.4.0,MIT atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT autoprefixer,6.7.7,MIT autoprefixer-rails,6.2.3,MIT +autosize,4.0.0,MIT +aws-sign2,0.6.0,Apache 2.0 +aws4,1.6.0,MIT axiom-types,0.1.1,MIT axios,0.16.2,MIT babel-code-frame,6.22.0,MIT @@ -130,6 +150,7 @@ babel-types,6.23.0,MIT babosa,1.0.2,MIT babylon,6.16.1,MIT backo2,1.0.2,MIT +balanced-match,0.4.2,MIT balanced-match,1.0.0,MIT base32,0.3.2,MIT base64-arraybuffer,0.1.5,MIT @@ -137,19 +158,25 @@ base64-js,1.2.0,MIT base64id,1.0.0,MIT batch,0.6.1,MIT bcrypt,3.1.11,MIT +bcrypt-pbkdf,1.0.1,New BSD bcrypt_pbkdf,1.0.0,MIT better-assert,1.0.2,MIT big.js,3.1.3,MIT binary-extensions,1.10.0,MIT bindata,2.4.1,ruby blob,0.0.4,unknown +block-stream,0.0.9,ISC bluebird,2.11.0,MIT +bluebird,3.5.0,MIT bn.js,4.11.6,MIT body-parser,1.17.2,MIT bonjour,3.5.0,MIT +boom,2.10.1,New BSD bootstrap-sass,3.3.6,MIT bootstrap_form,2.7.0,MIT +brace-expansion,1.1.7,MIT brace-expansion,1.1.8,MIT +braces,0.1.5,MIT braces,1.8.5,MIT brorand,1.0.7,MIT browser,2.2.0,MIT @@ -168,17 +195,23 @@ builder,3.2.3,MIT builtin-modules,1.1.1,MIT builtin-status-codes,3.0.0,MIT bytes,2.4.0,MIT +bytes,2.5.0,MIT caller-path,0.1.0,MIT callsite,1.0.0,unknown callsites,0.2.0,MIT +camelcase,1.2.1,MIT +camelcase,2.1.1,MIT +camelcase,3.0.0,MIT camelcase,4.1.0,MIT camelcase-keys,2.1.0,MIT caniuse-api,1.6.1,MIT caniuse-db,1.0.30000649,CC-BY-4.0 -carrierwave,1.1.0,MIT +carrierwave,1.2.1,MIT +caseless,0.12.0,Apache 2.0 cause,0.1,MIT center-align,0.1.3,MIT chalk,1.1.3,MIT +chalk,2.3.0,MIT charlock_holmes,0.7.5,MIT chokidar,1.7.0,MIT chronic,0.10.2,MIT @@ -191,6 +224,7 @@ clap,1.1.3,MIT cli-cursor,1.0.2,MIT cli-width,2.1.0,ISC clipboard,1.6.1,MIT +cliui,2.1.0,ISC cliui,3.2.0,ISC clone,1.0.2,MIT co,4.6.0,MIT @@ -204,9 +238,11 @@ color-string,0.3.0,MIT colormin,1.1.2,MIT colors,1.1.2,MIT combine-lists,1.0.1,MIT +combined-stream,1.0.5,MIT commander,2.9.0,MIT commondir,1.0.1,MIT component-bind,1.0.0,unknown +component-emitter,1.1.2,unknown component-emitter,1.2.1,MIT component-inherit,0.0.3,unknown compressible,2.0.11,MIT @@ -220,7 +256,8 @@ configstore,1.4.0,Simplified BSD connect,3.6.3,MIT connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT -console-browserify,1.1.0,[Circular] +console-browserify,1.1.0,MIT +console-control-strings,1.1.0,ISC consolidate,0.14.5,MIT constants-browserify,1.0.0,MIT contains-path,0.1.0,MIT @@ -230,6 +267,7 @@ convert-source-map,1.3.0,MIT cookie,0.3.1,MIT cookie-signature,1.0.6,MIT copy-webpack-plugin,4.0.1,MIT +core-js,2.3.0,MIT core-js,2.4.1,MIT core-util-is,1.0.2,MIT cosmiconfig,2.1.1,MIT @@ -240,9 +278,11 @@ create-hmac,1.1.4,MIT creole,0.5.0,ruby cropper,2.3.0,MIT cross-spawn,5.1.0,MIT +cryptiles,2.0.5,New BSD crypto-browserify,3.11.0,MIT css-color-names,0.0.4,MIT css-loader,0.28.0,MIT +css-selector-tokenizer,0.6.0,MIT css-selector-tokenizer,0.7.0,MIT css_parser,1.5.0,MIT cssesc,0.1.0,MIT @@ -250,11 +290,16 @@ cssnano,3.10.0,MIT csso,2.3.2,MIT currently-unhandled,0.4.1,MIT custom-event,1.0.1,MIT +d,0.1.1,MIT d,1.0.0,MIT d3,3.5.11,New BSD d3_rails,3.5.11,MIT +dashdash,1.14.1,MIT date-now,0.1.4,MIT de-indent,1.0.2,MIT +debug,2.2.0,MIT +debug,2.3.3,MIT +debug,2.6.7,MIT debug,2.6.8,MIT debugger-ruby_core_source,1.3.8,MIT decamelize,1.2.0,MIT @@ -269,7 +314,11 @@ default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT defined,1.0.0,MIT del,2.2.2,MIT +del,3.0.0,MIT +delayed-stream,1.0.0,MIT delegate,3.1.2,MIT +delegates,1.0.0,MIT +depd,1.1.0,MIT depd,1.1.1,MIT des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT @@ -285,12 +334,14 @@ diffy,3.1.0,MIT dns-equal,1.0.0,MIT dns-packet,1.2.2,MIT dns-txt,2.0.2,MIT +doctrine,1.5.0,BSD doctrine,2.0.0,Apache 2.0 document-register-element,1.3.0,MIT dom-serialize,2.2.1,MIT dom-serializer,0.1.0,MIT domain-browser,1.1.7,MIT domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" +domelementtype,1.1.3,unknown domelementtype,1.3.0,unknown domhandler,2.3.0,unknown domutils,1.5.1,unknown @@ -298,9 +349,10 @@ doorkeeper,4.2.6,MIT doorkeeper-openid_connect,1.2.0,MIT dropzone,4.2.0,MIT dropzonejs-rails,0.7.2,MIT -duplexer,0.1.1,[Circular] +duplexer,0.1.1,MIT duplexer3,0.1.4,New BSD duplexify,3.5.1,MIT +ecc-jsbn,0.1.1,MIT editorconfig,0.13.2,MIT ee-first,1.1.1,MIT ejs,2.5.6,Apache 2.0 @@ -315,6 +367,7 @@ end-of-stream,1.4.0,MIT engine.io,1.8.3,MIT engine.io-client,1.8.3,MIT engine.io-parser,1.3.2,MIT +enhanced-resolve,0.9.1,MIT enhanced-resolve,3.4.1,MIT ent,2.2.0,MIT entities,1.1.1,BSD-like @@ -346,9 +399,12 @@ eslint-plugin-jasmine,2.2.0,MIT eslint-plugin-promise,3.5.0,ISC espree,3.5.0,Simplified BSD esprima,2.7.3,Simplified BSD +esprima,4.0.0,Simplified BSD esquery,1.0.0,BSD esrecurse,4.1.0,Simplified BSD +estraverse,1.9.3,BSD estraverse,4.1.1,Simplified BSD +estraverse,4.2.0,Simplified BSD esutils,2.0.2,BSD et-orbi,1.0.3,MIT etag,1.8.0,MIT @@ -365,12 +421,14 @@ execjs,2.6.0,MIT exit-hook,1.1.1,MIT expand-braces,0.1.2,MIT expand-brackets,0.1.5,MIT +expand-range,0.1.1,MIT expand-range,1.8.2,MIT exports-loader,0.6.4,MIT express,4.15.4,MIT expression_parser,0.9.0,MIT extend,3.0.1,MIT extglob,0.3.2,MIT +extsprintf,1.0.2,MIT faraday,0.12.2,MIT faraday_middleware,0.11.0.1,MIT faraday_middleware-multi_json,0.0.6,MIT @@ -378,6 +436,8 @@ fast-deep-equal,1.0.0,MIT fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" fastparse,1.1.1,MIT +faye-websocket,0.10.0,MIT +faye-websocket,0.11.1,MIT faye-websocket,0.7.3,MIT ffi,1.9.18,New BSD figures,1.7.0,MIT @@ -386,17 +446,19 @@ file-loader,0.11.1,MIT filename-regex,2.0.0,MIT fileset,2.0.3,MIT filesize,3.3.0,New BSD +filesize,3.5.10,New BSD fill-range,2.2.3,MIT finalhandler,1.0.4,MIT find-cache-dir,1.0.0,MIT find-root,0.1.2,MIT +find-up,1.1.2,MIT find-up,2.1.0,MIT flat-cache,1.2.2,MIT flatten,1.0.2,MIT flipper,0.10.2,MIT flipper-active_record,0.10.2,MIT flowdock,0.7.1,MIT -fog-aliyun,0.1.0,MIT +fog-aliyun,0.2.0,MIT fog-aws,1.4.0,MIT fog-core,1.44.3,MIT fog-google,0.5.3,MIT @@ -409,6 +471,8 @@ follow-redirects,1.2.3,MIT font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" for-in,0.1.6,MIT for-own,0.1.4,MIT +forever-agent,0.6.1,Apache 2.0 +form-data,2.1.4,MIT formatador,0.2.5,MIT forwarded,0.1.0,MIT fresh,0.5.0,MIT @@ -416,8 +480,12 @@ from,0.1.7,MIT fs-access,1.0.1,MIT fs-extra,0.26.7,MIT fs.realpath,1.0.0,ISC -fsevents,,unknown +fsevents,1.1.2,MIT +fstream,1.0.11,ISC +fstream-ignore,1.0.5,ISC function-bind,1.1.0,MIT +fuzzaldrin-plus,0.5.0,MIT +gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT generate-function,2.0.0,MIT @@ -426,30 +494,37 @@ get-caller-file,1.0.2,ISC get-stdin,4.0.1,MIT get-stream,3.0.0,MIT get_process_mem,0.2.0,MIT +getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.41.0,MIT +gitaly-proto,0.51.0,MIT github-linguist,4.7.6,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT gitlab-grit,2.8.2,MIT -gitlab-markup,1.6.2,MIT +gitlab-markup,1.6.3,MIT gitlab-svgs,1.0.4,unknown gitlab_omniauth-ldap,2.0.4,MIT +glob,5.0.15,ISC glob,6.0.4,ISC +glob,7.1.1,ISC +glob,7.1.2,ISC glob-base,0.3.0,MIT glob-parent,2.0.0,ISC globalid,0.3.7,MIT globals,9.18.0,MIT globby,5.0.0,MIT +globby,6.1.0,MIT gollum-grit_adapter,1.0.1,MIT gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT good-listener,1.2.2,MIT google-api-client,0.13.6,Apache 2.0 -google-protobuf,3.4.0.2,New BSD +google-protobuf,3.4.1.1,New BSD +googleapis-common-protos-types,1.0.0,Apache 2.0 googleauth,0.5.3,Apache 2.0 +got,3.3.1,MIT got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ graceful-fs,4.1.11,ISC @@ -458,25 +533,31 @@ grape,1.0.0,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT grape_logging,1.7.0,MIT -grpc,1.6.0,Apache 2.0 +grpc,1.6.6,Apache 2.0 gzip-size,3.0.0,MIT hamlit,2.6.1,MIT handle-thing,1.2.5,MIT handlebars,4.0.6,MIT +har-schema,1.0.5,ISC +har-validator,4.2.1,ISC has,1.0.1,MIT has-ansi,2.0.0,MIT has-binary,0.1.7,MIT has-cors,1.1.0,MIT +has-flag,1.0.0,MIT has-flag,2.0.0,MIT has-symbol-support-x,1.3.0,MIT has-to-string-tag-x,1.3.0,MIT +has-unicode,2.0.1,ISC hash-sum,1.0.2,MIT hash.js,1.0.3,MIT hashie,3.5.6,MIT hashie-forbidden_attributes,0.1.1,MIT +hawk,3.1.3,New BSD he,1.1.1,MIT health_check,2.6.0,MIT hipchat,1.5.2,MIT +hoek,2.16.3,New BSD home-or-tmp,2.0.0,MIT hosted-git-info,2.2.0,ISC hpack.js,2.1.6,MIT @@ -489,10 +570,12 @@ htmlparser2,3.9.2,MIT http,0.9.8,MIT http-cookie,1.0.3,MIT http-deceiver,1.2.7,MIT +http-errors,1.6.1,MIT http-errors,1.6.2,MIT http-form_data,1.0.1,MIT http-proxy,1.16.2,MIT http-proxy-middleware,0.17.4,MIT +http-signature,1.1.1,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT httpclient,2.8.2,ruby @@ -513,6 +596,7 @@ indexof,0.0.1,unknown infinity-agent,2.0.3,MIT inflight,1.0.6,ISC influxdb,0.2.3,MIT +inherits,2.0.1,ISC inherits,2.0.3,ISC ini,1.3.4,ISC inquirer,0.12.0,MIT @@ -532,12 +616,16 @@ is-builtin-module,1.0.0,MIT is-dotfile,1.0.2,MIT is-equal-shallow,0.1.3,MIT is-extendable,0.1.1,MIT +is-extglob,1.0.0,MIT is-extglob,2.1.1,MIT is-finite,1.0.2,MIT +is-fullwidth-code-point,1.0.0,MIT is-fullwidth-code-point,2.0.0,MIT +is-glob,2.0.1,MIT is-glob,3.1.0,MIT is-my-json-valid,2.16.0,MIT is-npm,1.0.0,MIT +is-number,0.1.1,MIT is-number,2.1.0,MIT is-object,1.0.1,MIT is-path-cwd,1.0.0,MIT @@ -553,13 +641,16 @@ is-resolvable,1.0.0,MIT is-retry-allowed,1.1.0,MIT is-stream,1.1.0,MIT is-svg,2.1.0,MIT +is-typedarray,1.0.0,MIT is-unc-path,0.1.2,MIT is-utf8,0.2.1,MIT is-windows,0.2.0,MIT +isarray,0.0.1,MIT isarray,1.0.0,MIT isbinaryfile,3.0.2,MIT -isexe,2.0.0,ISC +isexe,1.1.2,ISC isobject,2.1.0,MIT +isstream,0.1.2,MIT istanbul,0.4.5,New BSD istanbul-api,1.1.1,New BSD istanbul-lib-coverage,1.0.1,New BSD @@ -573,6 +664,7 @@ jasmine-core,2.6.3,MIT jasmine-jquery,2.1.1,MIT jed,1.1.1,MIT jira-ruby,1.4.1,MIT +jodid25519,1.0.2,MIT jquery,2.2.1,MIT jquery-atwho-rails,1.3.2,MIT jquery-rails,4.1.1,MIT @@ -582,18 +674,23 @@ js-beautify,1.6.12,MIT js-cookie,2.1.3,MIT js-tokens,3.0.1,MIT js-yaml,3.7.0,MIT +js-yaml,3.9.1,MIT +jsbn,0.1.1,MIT +jsesc,0.5.0,MIT jsesc,1.3.0,MIT json,1.8.6,ruby json-jwt,1.7.2,MIT json-loader,0.5.7,MIT +json-schema,0.2.3,"AFLv2.1,BSD" json-schema-traverse,0.3.1,MIT json-stable-stringify,1.0.1,MIT json-stringify-safe,5.0.1,ISC -json3,3.3.2,[Circular] +json3,3.3.2,MIT json5,0.5.1,MIT jsonfile,2.4.0,MIT jsonify,0.0.0,Public Domain jsonpointer,4.0.1,MIT +jsprim,1.4.0,MIT jszip,3.1.3,(MIT OR GPL-3.0) jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT @@ -619,11 +716,14 @@ levn,0.3.0,MIT licensee,8.7.0,MIT lie,3.1.1,MIT little-plugger,1.1.4,MIT +load-json-file,1.1.0,MIT load-json-file,2.0.0,MIT loader-runner,2.3.0,MIT +loader-utils,0.2.16,MIT loader-utils,1.1.0,MIT locale,2.1.2,"ruby,LGPLv3+" locate-path,2.0.0,MIT +lodash,3.10.1,MIT lodash,4.17.4,MIT lodash._baseassign,3.2.0,MIT lodash._basecopy,3.0.1,MIT @@ -634,11 +734,13 @@ lodash._getnative,3.9.1,MIT lodash._isiterateecall,3.0.9,MIT lodash._topath,3.8.1,MIT lodash.assign,3.2.0,MIT +lodash.camelcase,4.1.1,MIT lodash.camelcase,4.3.0,MIT lodash.capitalize,4.2.1,MIT lodash.cond,4.5.2,MIT lodash.deburr,4.1.0,MIT lodash.defaults,3.1.2,MIT +lodash.get,3.7.0,MIT lodash.get,4.4.2,MIT lodash.isarguments,3.1.0,MIT lodash.isarray,3.0.4,MIT @@ -658,7 +760,9 @@ loofah,2.0.3,MIT loose-envify,1.3.1,MIT loud-rejection,1.6.0,MIT lowercase-keys,1.0.0,MIT +lru-cache,2.2.4,MIT lru-cache,3.2.0,ISC +lru-cache,4.0.2,ISC macaddress,0.2.8,MIT mail,2.6.6,MIT mail_room,0.9.1,MIT @@ -670,6 +774,7 @@ math-expression-evaluator,1.2.16,MIT media-typer,0.3.0,MIT mem,1.1.0,MIT memoist,0.16.0,MIT +memory-fs,0.2.0,MIT memory-fs,0.4.1,MIT meow,3.7.0,MIT merge-descriptors,1.0.1,MIT @@ -677,8 +782,10 @@ method_source,0.8.2,MIT methods,1.1.2,MIT micromatch,2.3.11,MIT miller-rabin,4.0.0,MIT -mime,1.3.4,[Circular] +mime,1.3.4,MIT +mime-db,1.27.0,MIT mime-db,1.29.0,MIT +mime-types,2.1.15,MIT mime-types,3.1,MIT mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT @@ -687,13 +794,17 @@ mimic-response,1.0.0,MIT mini_portile2,2.3.0,MIT minimalistic-assert,1.0.0,ISC minimatch,3.0.3,ISC +minimatch,3.0.4,ISC minimist,0.0.8,MIT +minimist,1.2.0,MIT mkdirp,0.5.1,MIT mmap2,2.2.7,ruby moment,2.17.1,MIT -monaco-editor,0.8.3,MIT +monaco-editor,0.10.0,MIT mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" +ms,0.7.1,MIT +ms,0.7.2,MIT ms,2.0.0,MIT multi_json,1.12.2,MIT multi_xml,0.6.0,MIT @@ -703,7 +814,9 @@ multipart-post,2.0.0,MIT mustermann,1.0.0,MIT mustermann-grape,1.0.0,MIT mute-stream,0.0.5,ISC +mysql2,0.4.5,MIT name-all-modules-plugin,1.0.1,MIT +nan,2.6.2,MIT natural-compare,1.4.0,MIT negotiator,0.6.1,MIT nested-error-stacks,1.0.2,MIT @@ -712,21 +825,30 @@ net-ssh,4.1.0,MIT netrc,0.11.0,MIT node-dir,0.1.17,MIT node-forge,0.6.33,BSD +node-libs-browser,1.1.1,MIT node-libs-browser,2.0.0,MIT +node-pre-gyp,0.6.36,New BSD +node-pre-gyp,0.6.37,New BSD nodemon,1.11.0,MIT nokogiri,1.8.1,MIT +nopt,1.0.10,MIT nopt,3.0.6,ISC -normalize-package-data,2.3.5,Simplified BSD +nopt,4.0.1,ISC +normalize-package-data,2.4.0,Simplified BSD normalize-path,2.1.1,MIT normalize-range,0.1.2,MIT normalize-url,1.9.1,MIT npm-run-path,2.0.2,MIT +npmlog,4.1.0,ISC null-check,1.0.0,MIT num2fraction,1.2.2,MIT number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT oauth,0.5.1,MIT +oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT +object-assign,3.0.0,MIT +object-assign,4.1.0,MIT object-assign,4.1.1,MIT object-component,0.0.3,unknown object.omit,2.0.1,MIT @@ -766,6 +888,7 @@ orm_adapter,0.5.0,MIT os,0.9.6,MIT os-browserify,0.2.1,MIT os-homedir,1.0.2,MIT +os-locale,1.4.0,MIT os-locale,2.1.0,MIT os-tmpdir,1.0.2,MIT osenv,0.1.4,ISC @@ -776,6 +899,7 @@ p-locate,2.0.0,MIT p-map,1.1.1,MIT p-timeout,1.2.0,MIT package-json,1.2.0,MIT +pako,0.2.9,MIT pako,1.0.5,(MIT AND Zlib) paranoia,2.3.1,MIT parse-asn1,5.0.0,ISC @@ -786,28 +910,34 @@ parseqs,0.0.5,MIT parseuri,0.0.5,MIT parseurl,1.3.1,MIT path-browserify,0.0.0,MIT +path-exists,2.1.0,MIT path-exists,3.0.0,MIT path-is-absolute,1.0.1,MIT path-is-inside,1.0.2,(WTFPL OR MIT) path-key,2.0.1,MIT path-parse,1.0.5,MIT path-to-regexp,0.1.7,MIT +path-type,1.1.0,MIT path-type,2.0.0,MIT pause-stream,0.0.11,"MIT,Apache2" pbkdf2,3.0.9,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT peek-host,1.0.0,MIT +peek-mysql2,1.1.0,MIT peek-performance_bar,1.3.0,MIT peek-pg,1.3.0,MIT peek-rblineprof,0.2.0,MIT peek-redis,1.2.0,MIT peek-sidekiq,1.0.3,MIT +performance-now,0.2.0,MIT pg,0.18.4,"BSD,ruby,GPL" pify,2.3.0,MIT -pikaday,1.5.1,"BSD,MIT" +pify,3.0.0,MIT +pikaday,1.6.1,MIT pinkie,2.0.4,MIT pinkie-promise,2.0.1,MIT +pkg-dir,1.0.0,MIT pkg-dir,2.0.0,MIT pkg-up,1.0.0,MIT pluralize,1.2.1,MIT @@ -860,7 +990,7 @@ private,0.1.7,MIT process,0.11.9,MIT process-nextick-args,1.0.7,MIT progress,1.1.8,MIT -prometheus-client-mmap,0.7.0.beta14,Apache 2.0 +prometheus-client-mmap,0.7.0.beta18,Apache 2.0 proto-list,1.2.4,ISC proxy-addr,1.1.5,MIT prr,0.0.0,MIT @@ -868,15 +998,18 @@ ps-tree,1.1.0,MIT pseudomap,1.0.2,ISC public-encrypt,4.0.0,MIT public_suffix,3.0.0,MIT +punycode,1.3.2,MIT punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT q,1.5.0,MIT qjobs,1.1.5,MIT +qs,6.4.0,New BSD qs,6.5.0,New BSD query-string,4.3.2,MIT querystring,0.2.0,MIT -querystring-es3,0.2.1,[Circular] +querystring-es3,0.2.1,MIT querystringify,0.0.4,MIT +querystringify,1.0.0,MIT rack,1.6.8,MIT rack-accept,0.4.5,MIT rack-attack,4.4.1,MIT @@ -908,9 +1041,14 @@ rdoc,4.2.2,ruby re2,1.1.1,New BSD react-dev-utils,0.5.2,New BSD read-all-stream,3.1.0,MIT +read-pkg,1.1.0,MIT read-pkg,2.0.0,MIT +read-pkg-up,1.0.1,MIT read-pkg-up,2.0.0,MIT +readable-stream,1.0.34,MIT readable-stream,2.0.6,MIT +readable-stream,2.2.9,MIT +readable-stream,2.3.3,MIT readdirp,2.1.0,MIT readline2,1.0.1,MIT recaptcha,3.0.0,MIT @@ -932,21 +1070,26 @@ regenerate,1.3.2,MIT regenerator-runtime,0.10.1,MIT regenerator-transform,0.9.8,BSD regex-cache,0.4.3,MIT +regexpu-core,1.0.0,MIT regexpu-core,2.0.0,MIT registry-url,3.1.0,MIT regjsgen,0.2.0,MIT regjsparser,0.1.5,BSD remove-trailing-separator,1.1.0,ISC repeat-element,1.1.2,MIT +repeat-string,0.2.2,MIT repeat-string,1.6.1,MIT +repeating,1.1.3,MIT repeating,2.0.1,MIT representable,3.0.4,MIT +request,2.81.0,Apache 2.0 request_store,1.3.1,MIT require-directory,2.1.1,MIT require-from-string,1.2.1,MIT require-main-filename,1.0.1,ISC require-uncached,1.0.3,MIT requires-port,1.0.0,MIT +resolve,1.1.7,MIT resolve,1.2.0,MIT resolve-from,1.0.1,MIT responders,2.3.0,MIT @@ -972,6 +1115,7 @@ rugged,0.26.0,MIT run-async,0.1.0,MIT rx-lite,3.1.2,Apache 2.0 safe-buffer,5.0.1,MIT +safe-buffer,5.1.1,MIT safe_yaml,1.0.4,MIT sanitize,2.1.0,MIT sass,3.4.22,MIT @@ -985,6 +1129,7 @@ select-hose,2.0.0,MIT select2,3.5.2-browserify,unknown select2-rails,3.5.9.3,MIT selfsigned,1.10.1,MIT +semver,4.3.6,ISC semver,5.3.0,ISC semver-diff,2.1.0,MIT send,0.15.4,MIT @@ -1011,14 +1156,20 @@ slack-notifier,1.5.1,MIT slash,1.0.0,MIT slice-ansi,0.0.4,MIT slide,1.1.6,ISC +sntp,1.0.9,BSD socket.io,1.7.3,MIT socket.io-adapter,0.5.0,MIT socket.io-client,1.7.3,MIT socket.io-parser,2.3.1,MIT sockjs,0.3.18,MIT sockjs-client,1.0.1,MIT +sockjs-client,1.1.4,MIT sort-keys,1.1.2,MIT +source-list-map,0.1.8,MIT source-list-map,2.0.0,MIT +source-map,0.1.43,BSD +source-map,0.2.0,BSD +source-map,0.4.4,New BSD source-map,0.5.6,New BSD source-map-support,0.4.11,MIT spdx-correct,1.0.2,Apache 2.0 @@ -1031,6 +1182,7 @@ sprintf-js,1.0.3,New BSD sprockets,3.7.1,MIT sprockets-rails,3.2.0,MIT sql.js,0.4.0,MIT +sshpk,1.13.0,MIT state_machines,0.4.0,MIT state_machines-activemodel,0.4.0,MIT state_machines-activerecord,0.4.0,MIT @@ -1042,19 +1194,29 @@ stream-shift,1.0.0,MIT strict-uri-encode,1.1.0,MIT string-length,1.0.1,MIT string-width,1.0.2,MIT +string-width,2.0.0,MIT string_decoder,0.10.31,MIT +string_decoder,1.0.1,MIT +string_decoder,1.0.3,MIT stringex,2.7.1,MIT +stringstream,0.0.5,MIT strip-ansi,3.0.1,MIT +strip-bom,2.0.0,MIT strip-bom,3.0.0,MIT strip-eof,1.0.0,MIT strip-indent,1.0.1,MIT strip-json-comments,2.0.1,MIT +supports-color,2.0.0,MIT supports-color,3.2.3,MIT +supports-color,4.2.1,MIT svg4everybody,2.1.9,CC0-1.0 svgo,0.7.2,MIT sys-filesystem,1.1.6,Artistic 2.0 table,3.8.3,New BSD +tapable,0.1.10,MIT tapable,0.2.8,MIT +tar,2.2.1,ISC +tar-pack,3.4.0,Simplified BSD temple,0.7.7,MIT test-exclude,4.0.0,ISC text,1.3.1,MIT @@ -1067,9 +1229,10 @@ three-stl-loader,1.0.4,MIT through,2.3.8,MIT thunky,0.1.0,unknown tilt,2.0.6,MIT -time-stamp,2.0.0,MIT timeago.js,2.0.5,MIT +timed-out,2.0.0,MIT timed-out,4.0.1,MIT +timers-browserify,1.4.2,MIT timers-browserify,2.0.4,MIT timfel-krb5-auth,0.8.3,LGPL tiny-emitter,1.1.0,MIT @@ -1079,15 +1242,20 @@ to-arraybuffer,1.0.1,MIT to-fast-properties,1.0.2,MIT toml-rb,0.3.15,MIT touch,1.0.0,ISC +tough-cookie,2.3.2,New BSD traverse,0.6.6,MIT trim-newlines,1.0.0,MIT trim-right,1.0.1,MIT truncato,0.7.10,MIT tryit,1.0.3,MIT +ts-loader,3.1.1,MIT tty-browserify,0.0.0,MIT +tunnel-agent,0.6.0,Apache 2.0 +tweetnacl,0.14.5,Unlicense type-check,0.3.2,MIT type-is,1.6.15,MIT typedarray,0.0.6,MIT +typescript,2.6.1,Apache 2.0 tzinfo,1.2.3,MIT u2f,0.2.1,MIT uber,0.1.0,MIT @@ -1095,6 +1263,8 @@ uglifier,2.7.2,MIT uglify-js,2.8.29,Simplified BSD uglify-to-browserify,1.0.2,MIT uglifyjs-webpack-plugin,0.4.6,MIT +uid-number,0.0.6,ISC +ultron,1.0.2,MIT ultron,1.1.0,MIT unc-path-regex,0.1.2,MIT undefsafe,0.0.3,MIT / http://rem.mit-license.org @@ -1111,6 +1281,8 @@ update-notifier,0.5.0,Simplified BSD url,0.11.0,MIT url-loader,0.5.8,MIT url-parse,1.0.5,MIT +url-parse,1.1.7,MIT +url-parse,1.1.9,MIT url-parse-lax,1.0.0,MIT url-to-options,1.0.1,MIT url_safe_base64,0.2.2,MIT @@ -1118,26 +1290,28 @@ user-home,2.0.0,MIT useragent,2.2.1,MIT util,0.10.3,MIT util-deprecate,1.0.2,MIT -utils-merge,1.0.0,[Circular] +utils-merge,1.0.0,MIT uuid,2.0.3,MIT +uuid,3.0.1,MIT validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT vary,1.1.1,MIT vendors,1.0.1,MIT +verror,1.3.6,MIT version_sorter,2.1.0,MIT virtus,1.0.5,MIT visibilityjs,1.2.4,MIT vm-browserify,0.0.4,MIT vmstat,2.3.0,MIT void-elements,2.0.1,MIT -vue,2.2.6,MIT +vue,2.5.2,MIT vue-hot-reload-api,2.0.11,MIT vue-loader,11.3.4,MIT vue-resource,1.3.4,MIT vue-style-loader,2.0.5,MIT -vue-template-compiler,2.2.6,MIT +vue-template-compiler,2.5.2,MIT vue-template-es2015-compiler,1.5.1,MIT -vuex,2.3.1,MIT +vuex,3.0.0,MIT warden,1.2.6,MIT watchpack,1.4.0,MIT wbuf,1.7.2,MIT @@ -1151,15 +1325,20 @@ webpack-stats-plugin,0.1.5,MIT websocket-driver,0.6.5,MIT websocket-extensions,0.1.1,MIT whet.extend,0.9.9,MIT -which,1.3.0,ISC +which,1.2.12,ISC +which-module,1.0.0,ISC which-module,2.0.0,ISC +wide-align,1.1.2,ISC wikicloth,0.8.1,MIT window-size,0.1.0,MIT +wordwrap,0.0.2,MIT/X11 +wordwrap,0.0.3,MIT wordwrap,1.0.0,MIT wrap-ansi,2.1.0,MIT wrappy,1.0.2,ISC write,0.2.1,MIT write-file-atomic,1.3.4,ISC +ws,1.1.2,MIT ws,2.3.1,MIT wtf-8,1.0.0,MIT xdg-basedir,2.0.0,MIT @@ -1168,6 +1347,9 @@ xmlhttprequest-ssl,1.5.3,MIT xtend,4.0.1,MIT y18n,3.2.1,ISC yallist,2.1.2,ISC +yargs,3.10.0,MIT +yargs,6.6.0,MIT yargs,8.0.2,MIT +yargs-parser,4.2.1,ISC yargs-parser,7.0.0,ISC yeast,0.1.2,MIT diff --git a/yarn.lock b/yarn.lock index efae3892c31..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" @@ -101,12 +105,6 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" - dependencies: - color-convert "^1.9.0" - anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -223,12 +221,18 @@ async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@2.4.1, async@^2.1.2, async@^2.1.4: +async@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" +async@^2.1.2, async@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -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" @@ -267,7 +277,7 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: +babel-code-frame@^6.11.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -275,6 +285,14 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.0" +babel-code-frame@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + babel-core@^6.22.1, babel-core@^6.23.0: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df" @@ -924,7 +942,11 @@ bluebird@^2.10.2: version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" -bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0: +bluebird@^3.0.5, bluebird@^3.1.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + +bluebird@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -1055,6 +1077,10 @@ buffer-indexof@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982" +buffer-shims@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + buffer-xor@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1154,14 +1180,6 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" - dependencies: - ansi-styles "^3.1.0" - escape-string-regexp "^1.0.5" - supports-color "^4.0.0" - chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1245,7 +1263,7 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -color-convert@^1.3.0, color-convert@^1.9.0: +color-convert@^1.3.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" dependencies: @@ -1446,7 +1464,11 @@ copy-webpack-plugin@^4.0.1: minimatch "^3.0.0" node-dir "^0.1.10" -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1: +core-js@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086" + +core-js@^2.4.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -1985,7 +2007,7 @@ engine.io@1.8.3: engine.io-parser "1.3.2" ws "1.1.2" -enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0: +enhanced-resolve@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" dependencies: @@ -2066,7 +2088,11 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" -es6-promise@^3.0.2, es6-promise@~3.0.2: +es6-promise@^3.0.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + +es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" @@ -2247,6 +2273,10 @@ esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" +esprima@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -2647,11 +2677,11 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.0.2, function-bind@~1.1.0: +function-bind@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" -function-bind@^1.1.1: +function-bind@^1.1.1, function-bind@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2700,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" @@ -2737,25 +2763,25 @@ glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" +glob@^7.0.0, glob@^7.1.1, glob@~7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.2" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" -glob@~7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^7.0.3, glob@^7.0.5: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.0.2" once "^1.3.0" path-is-absolute "^1.0.0" @@ -2958,10 +2984,14 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" -html-entities@1.2.0, html-entities@^1.2.0: +html-entities@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + htmlparser2@^3.8.2: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" @@ -3367,6 +3397,10 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -3510,7 +3544,18 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0: +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + +js-yaml@^3.5.1: version "3.9.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" dependencies: @@ -4051,7 +4096,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.29.0 < 2": +"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0: version "1.29.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878" @@ -4059,7 +4104,13 @@ mime-db@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23" + dependencies: + mime-db "~1.29.0" + +mime-types@~2.1.11, mime-types@~2.1.15: version "2.1.15" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" dependencies: @@ -4283,7 +4334,16 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.3.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^2.3.4: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: @@ -4458,7 +4518,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5154,7 +5214,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.0, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -5177,6 +5237,18 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~0.10.x" util-deprecate "~1.0.1" +readable-stream@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" + dependencies: + buffer-shims "^1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readable-stream@~1.0.2: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -5462,10 +5534,14 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@^5.0.3: + version "5.4.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" + semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -5873,7 +5949,7 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co dependencies: has-flag "^1.0.0" -supports-color@^4.0.0, supports-color@^4.2.1: +supports-color@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" dependencies: @@ -5987,6 +6063,10 @@ thunky@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + timeago.js@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c" @@ -6015,12 +6095,18 @@ tiny-emitter@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb" -tmp@0.0.31, tmp@0.0.x: +tmp@0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" +tmp@0.0.x: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" @@ -6061,15 +6147,6 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" -ts-loader@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.1.1.tgz#602d93c12029eaf8fa1ee478a90785d40c5f6658" - dependencies: - chalk "^2.3.0" - enhanced-resolve "^3.0.0" - loader-utils "^1.0.2" - semver "^5.0.1" - tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -6101,10 +6178,6 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" - uglify-js@^2.6, uglify-js@^2.8.29: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" @@ -6380,7 +6453,7 @@ webpack-bundle-analyzer@^2.8.2: opener "^1.4.3" ws "^2.3.1" -webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: +webpack-dev-middleware@^1.0.11: version "1.11.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9" dependencies: @@ -6389,6 +6462,16 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" +webpack-dev-middleware@^1.11.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" + dependencies: + memory-fs "~0.4.1" + mime "^1.3.4" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + time-stamp "^2.0.0" + webpack-dev-server@^2.6.1: version "2.7.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8" @@ -6477,12 +6560,18 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.1.1, which@^1.2.1, which@^1.2.9: +which@^1.1.1, which@^1.2.1: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: isexe "^1.1.1" +which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" |