diff options
38 files changed, 277 insertions, 190 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 70a71baa590..59eec634e8b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -10,6 +10,7 @@ plugins: - import - "@gitlab/i18n" - "@gitlab/vue-i18n" + - no-jquery settings: import/resolver: webpack: @@ -36,6 +37,11 @@ rules: vue/no-use-v-if-with-v-for: off vue/no-v-html: off vue/use-v-on-exact: off + no-jquery/no-ajax: error + no-jquery/no-ajax-events: error + no-jquery/no-load: error + no-jquery/no-load-shorthand: error + no-jquery/no-serialize: error overrides: files: - '**/spec/**/*' diff --git a/.gitlab/issue_templates/Acceptance_Testing.md b/.gitlab/issue_templates/Acceptance_Testing.md index f1fbb96ce61..5a6c35f28ad 100644 --- a/.gitlab/issue_templates/Acceptance_Testing.md +++ b/.gitlab/issue_templates/Acceptance_Testing.md @@ -25,7 +25,7 @@ Then leave running while monitoring and performing some testing through web, api - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved) ## 2. Staging Trial @@ -41,7 +41,7 @@ Then leave running while monitoring for at least **15 minutes** while performing - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved) ## 4. Production Server Version Check @@ -57,7 +57,7 @@ Then leave running while monitoring for at least **15 minutes** while performing - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved) ## 6. Low Impact Check @@ -69,7 +69,7 @@ Then leave running while monitoring for at least **30 minutes** while performing - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved) ## 7. Mid Impact Trial @@ -81,7 +81,7 @@ Then leave running while monitoring for at least **12 hours** while performing s - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved) ## 8. Full Impact Trial @@ -93,7 +93,7 @@ Then leave running while monitoring for at least **1 week**. - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) -- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved) #### Success? @@ -446,7 +446,7 @@ group :ed25519 do end # Gitaly GRPC protocol definitions -gem 'gitaly', '~> 1.58.0' +gem 'gitaly', '~> 1.65.0' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7b450c262da..3142ba094cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,7 +358,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) git (1.5.0) - gitaly (1.58.0) + gitaly (1.65.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-labkit (0.5.2) @@ -1168,7 +1168,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly (~> 1.58.0) + gitaly (~> 1.65.0) github-markup (~> 1.7.0) gitlab-labkit (~> 0.5) gitlab-license (~> 1.0) diff --git a/app/assets/javascripts/event_tracking/issue_sidebar.js b/app/assets/javascripts/event_tracking/issue_sidebar.js deleted file mode 100644 index 6909f82c66f..00000000000 --- a/app/assets/javascripts/event_tracking/issue_sidebar.js +++ /dev/null @@ -1,2 +0,0 @@ -export const initSidebarTracking = () => {}; -export const trackEvent = () => {}; diff --git a/app/assets/javascripts/event_tracking/notes.js b/app/assets/javascripts/event_tracking/notes.js deleted file mode 100644 index 1f70290c397..00000000000 --- a/app/assets/javascripts/event_tracking/notes.js +++ /dev/null @@ -1,2 +0,0 @@ -// Noop function which has a EE counter-part -export default () => {}; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 77080691dcb..c21fba06d42 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -22,6 +22,7 @@ export default class FilterableList { getPagePath() { const action = this.filterForm.getAttribute('action'); + // eslint-disable-next-line no-jquery/no-serialize const params = $(this.filterForm).serialize(); return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`; } diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index a7746bb3a0b..1c9b94ade8a 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -42,6 +42,7 @@ export default class IntegrationSettingsForm { // and test the service using provided configuration. if (this.$form.get(0).checkValidity() && this.canTestService) { e.preventDefault(); + // eslint-disable-next-line no-jquery/no-serialize this.testSettings(this.$form.serialize()); } } diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 4bcba72bc23..e170d338408 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar'; import issuableApp from './components/app.vue'; import { parseIssuableData } from './utils/parse_data'; @@ -9,9 +8,6 @@ export default function initIssueableApp() { components: { issuableApp, }, - mounted() { - initSidebarTracking(); - }, render(createElement) { return createElement('issuable-app', { props: parseIssuableData(), diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 49be11d466d..c19a845eb69 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -314,6 +314,7 @@ document.addEventListener('DOMContentLoaded', () => { const action = `${this.action}${link.search === '' ? '?' : '&'}`; event.preventDefault(); + // eslint-disable-next-line no-jquery/no-serialize visitUrl(`${action}${$(this).serialize()}`); }); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9cc56b34c75..ed52eec8b18 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1461,6 +1461,7 @@ export default class Notes { getFormData($form) { const content = $form.find('.js-note-text').val(); return { + // eslint-disable-next-line no-jquery/no-serialize formData: $form.serialize(), formContent: _.escape(content), formAction: $form.attr('action'), diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index 1aeb07d6608..8bdee759a23 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -19,7 +19,9 @@ export default { <gl-button ref="button" v-gl-tooltip - class="note-action-button js-note-action-reply" + class="note-action-button" + data-track-event="click_button" + data-track-label="reply_comment_button" variant="transparent" :title="__('Reply to comment')" @click="$emit('startReplying')" diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index c70c0e4095c..30372103590 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import initNoteStats from 'ee_else_ce/event_tracking/notes'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; import createStore from './stores'; @@ -39,9 +38,6 @@ document.addEventListener('DOMContentLoaded', () => { notesData: JSON.parse(notesDataset.notesData), }; }, - mounted() { - initNoteStats(); - }, render(createElement) { return createElement('notes-app', { props: { diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js index 8cc3cb0a57c..9f08260c3d6 100644 --- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + // eslint-disable-next-line no-jquery/no-load $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); initPipelines(); }); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 6fc982967eb..5aa4734244e 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => { }).bindEvents(); initNotes(); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); + // eslint-disable-next-line no-jquery/no-load $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); fetchCommitMergeRequests(); initDiffNotes(); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 63b93a80ead..f4dac38b9e1 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,6 +1,5 @@ <script> import { n__ } from '~/locale'; -import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { name: 'AssigneeTitle', @@ -30,11 +29,6 @@ export default { return n__('Assignee', `%d Assignees`, assignees); }, }, - methods: { - trackEdit() { - trackEvent('click_edit_button', 'assignee'); - }, - }, }; </script> <template> @@ -45,7 +39,9 @@ export default { v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#" - @click.prevent="trackEdit" + data-track-event="click_edit_button" + data-track-label="right_sidebar" + data-track-property="assignee" > {{ __('Edit') }} </a> 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 1c75b6148e8..e350264de96 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -5,7 +5,6 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; -import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { components: { @@ -52,11 +51,6 @@ export default { toggleForm() { this.edit = !this.edit; }, - onEditClick() { - this.toggleForm(); - - trackEvent('click_edit_button', 'confidentiality'); - }, updateConfidentialAttribute(confidential) { this.service .update('issue', { confidential }) @@ -88,7 +82,10 @@ export default { v-if="isEditable" class="float-right confidential-edit" href="#" - @click.prevent="onEditClick" + data-track-event="click_edit_button" + data-track-label="right_sidebar" + data-track-property="confidentiality" + @click.prevent="toggleForm" > {{ __('Edit') }} </a> 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 ec2a7b93a98..c7c5e0e20f1 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -6,7 +6,6 @@ import issuableMixin from '~/vue_shared/mixins/issuable'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; -import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { components: { @@ -66,11 +65,6 @@ export default { toggleForm() { this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; }, - onEditClick() { - this.toggleForm(); - - trackEvent('click_edit_button', 'lock_issue'); - }, updateLockedAttribute(locked) { this.mediator.service .update(this.issuableType, { @@ -114,7 +108,10 @@ export default { v-if="isEditable" class="float-right lock-edit" type="button" - @click.prevent="onEditClick" + data-track-event="click_edit_button" + data-track-label="right_sidebar" + data-track-property="lock_issue" + @click.prevent="toggleForm" > {{ __('Edit') }} </button> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 1f5f19d1931..ea5edb3ce3f 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,10 +1,10 @@ <script> import { __ } from '~/locale'; +import Tracking from '~/tracking'; import icon from '~/vue_shared/components/icon.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; -import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; const ICON_ON = 'notifications'; const ICON_OFF = 'notifications-off'; @@ -19,6 +19,7 @@ export default { icon, toggleButton, }, + mixins: [Tracking.mixin({ label: 'right_sidebar' })], props: { loading: { type: Boolean, @@ -65,7 +66,10 @@ export default { // Component event emission. this.$emit('toggleSubscription', this.id); - trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1); + this.track('toggle_button', { + property: 'notifications', + value: this.subscribed ? 0 : 1, + }); }, onClickCollapsedIcon() { this.$emit('toggleSidebar'); diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index 1b4ca1d5741..7c0097fbe37 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -1,4 +1,4 @@ -import $ from 'jquery'; +import _ from 'underscore'; const DEFAULT_SNOWPLOW_OPTIONS = { namespace: 'gl', @@ -14,18 +14,31 @@ const DEFAULT_SNOWPLOW_OPTIONS = { linkClickTracking: false, }; -const extractData = (el, opts = {}) => { - const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset; - let trackValue = el.dataset.trackValue || el.value || ''; - if (el.type === 'checkbox' && !el.checked) trackValue = false; - return [ - trackEvent + (opts.suffix || ''), - { - label: trackLabel, - property: trackProperty, - value: trackValue, - }, - ]; +const eventHandler = (e, func, opts = {}) => { + const el = e.target.closest('[data-track-event]'); + const action = el && el.dataset.trackEvent; + if (!action) return; + + let value = el.dataset.trackValue || el.value || undefined; + if (el.type === 'checkbox' && !el.checked) value = false; + + const data = { + label: el.dataset.trackLabel, + property: el.dataset.trackProperty, + value, + context: el.dataset.trackContext, + }; + + func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined)); +}; + +const eventHandlers = (category, func) => { + const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts }); + const handlers = []; + handlers.push({ name: 'click', func: handler() }); + handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) }); + handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) }); + return handlers; }; export default class Tracking { @@ -39,49 +52,43 @@ export default class Tracking { return typeof window.snowplow === 'function' && this.trackable(); } - static event(category = document.body.dataset.page, event = 'generic', data = {}) { + static event(category = document.body.dataset.page, action = 'generic', data = {}) { if (!this.enabled()) return false; // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings if (!category) throw new Error('Tracking: no category provided for tracking.'); - return window.snowplow( - 'trackStructEvent', - category, - event, - Object.assign({}, { label: '', property: '', value: '' }, data), - ); + const { label, property, value, context } = data; + const contexts = context ? [context] : undefined; + return window.snowplow('trackStructEvent', category, action, label, property, value, contexts); } - constructor(category = document.body.dataset.page) { - this.category = category; - } - - bind(container = document) { - if (!this.constructor.enabled()) return; - container.querySelectorAll(`[data-track-event]`).forEach(el => { - if (this.customHandlingFor(el)) return; - // jquery is required for select2, so we use it always - // see: https://github.com/select2/select2/issues/4686 - $(el).on('click', this.eventHandler(this.category)); - }); - } + static bindDocument(category = document.body.dataset.page, documentOverride = null) { + const el = documentOverride || document; + if (!this.enabled() || el.trackingBound) return []; - customHandlingFor(el) { - const classes = el.classList; + el.trackingBound = true; - // bootstrap dropdowns - if (classes.contains('dropdown')) { - $(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' })); - $(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' })); - return true; - } - - return false; + const handlers = eventHandlers(category, (...args) => this.event(...args)); + handlers.forEach(event => el.addEventListener(event.name, event.func)); + return handlers; } - eventHandler(category = null, opts = {}) { - return e => { - this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts)); + static mixin(opts) { + return { + data() { + return { + tracking: { + // eslint-disable-next-line no-underscore-dangle + category: this.$options.name || this.$options._componentTag, + }, + }; + }, + methods: { + track(action, data) { + const category = opts.category || data.category || this.tracking.category; + Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data }); + }, + }, }; } } @@ -89,7 +96,7 @@ export default class Tracking { export function initUserTracking() { if (!Tracking.enabled()) return; - const opts = Object.assign({}, DEFAULT_SNOWPLOW_OPTIONS, window.snowplowOptions); + const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions }; window.snowplow('newTracker', opts.namespace, opts.hostname, opts); window.snowplow('enableActivityTracking', 30, 30); @@ -97,4 +104,6 @@ export function initUserTracking() { if (opts.formTracking) window.snowplow('enableFormTracking'); if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); + + Tracking.bindDocument(); } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 7e7b08797b2..13a2a74fdab 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); position: sticky; position: -webkit-sticky; top: $flash-container-top; - z-index: 200; + z-index: 251; .flash-content { box-shadow: 0 2px 4px 0 $notification-box-shadow-color; diff --git a/app/models/repository.rb b/app/models/repository.rb index f084a314392..96b1b55e2b1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,18 +133,28 @@ class Repository end end - def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil) + # the opts are: + # - :path + # - :limit + # - :offset + # - :skip_merges + # - :after + # - :before + # - :all + # - :first_parent + def commits(ref = nil, opts = {}) options = { repo: raw_repository, ref: ref, - path: path, - limit: limit, - offset: offset, - after: after, - before: before, - follow: Array(path).length == 1, - skip_merges: skip_merges, - all: all + path: opts[:path], + follow: Array(opts[:path]).length == 1, + limit: opts[:limit], + offset: opts[:offset], + skip_merges: !!opts[:skip_merges], + after: opts[:after], + before: opts[:before], + all: !!opts[:all], + first_parent: !!opts[:first_parent] } commits = Gitlab::Git::Commit.where(options) diff --git a/changelogs/unreleased/add-first-parent-to-find-commits.yml b/changelogs/unreleased/add-first-parent-to-find-commits.yml new file mode 100644 index 00000000000..076eed90f68 --- /dev/null +++ b/changelogs/unreleased/add-first-parent-to-find-commits.yml @@ -0,0 +1,5 @@ +--- +title: Add first_parent option to list commits api +merge_request: 32410 +author: jhenkens +type: added diff --git a/config/application.rb b/config/application.rb index c1e3b6f7a20..5d7c52c5d81 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,6 +13,7 @@ Bundler.require(*Rails.groups) module Gitlab class Application < Rails::Application require_dependency Rails.root.join('lib/gitlab') + require_dependency Rails.root.join('lib/gitlab/utils') require_dependency Rails.root.join('lib/gitlab/redis/wrapper') require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/queues') @@ -46,18 +47,20 @@ module Gitlab config.generators.templates.push("#{config.root}/generator_templates") - ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo| - ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root)) - memo << ee_path.to_s if ee_path.exist? - end + if Gitlab.ee? + ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo| + ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root)) + memo << ee_path.to_s + end - # Eager load should load CE first - config.eager_load_paths.push(*ee_paths) - config.helpers_paths.push "#{config.root}/ee/app/helpers" + # Eager load should load CE first + config.eager_load_paths.push(*ee_paths) + config.helpers_paths.push "#{config.root}/ee/app/helpers" - # Other than Ruby modules we load EE first - config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks" - config.paths['app/views'].unshift "#{config.root}/ee/app/views" + # Other than Ruby modules we load EE first + config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks" + config.paths['app/views'].unshift "#{config.root}/ee/app/views" + end # Rake tasks ignore the eager loading settings, so we need to set the # autoload paths explicitly @@ -178,16 +181,18 @@ module Gitlab config.assets.paths << "#{config.root}/node_modules/xterm/src/" config.assets.precompile << "xterm.css" - %w[images javascripts stylesheets].each do |path| - config.assets.paths << "#{config.root}/ee/app/assets/#{path}" - config.assets.precompile << "jira_connect.js" - config.assets.precompile << "pages/jira_connect.css" + if Gitlab.ee? + %w[images javascripts stylesheets].each do |path| + config.assets.paths << "#{config.root}/ee/app/assets/#{path}" + config.assets.precompile << "jira_connect.js" + config.assets.precompile << "pages/jira_connect.css" + end end # Import path for EE specific SCSS entry point # In CE it will import a noop file, in EE a functioning file # Order is important, so that the ee file takes precedence: - config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee" + config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee" if Gitlab.ee? config.assets.paths << "#{config.root}/app/assets/stylesheets/_ee" config.assets.paths << "#{config.root}/vendor/assets/javascripts/" @@ -197,13 +202,15 @@ module Gitlab # See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508 config.assets.paths << "#{config.root}/node_modules" - # Compile non-JS/CSS assets in the ee/app/assets folder by default - # Mimic sprockets-rails default: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87 - LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename| - filename.start_with?(config.root.join("ee/app/assets").to_s) && - !['.js', '.css', ''].include?(File.extname(logical_path)) + if Gitlab.ee? + # Compile non-JS/CSS assets in the ee/app/assets folder by default + # Mimic sprockets-rails default: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87 + LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename| + filename.start_with?(config.root.join("ee/app/assets").to_s) && + !['.js', '.css', ''].include?(File.extname(logical_path)) + end + config.assets.precompile << LOOSE_EE_APP_ASSETS end - config.assets.precompile << LOOSE_EE_APP_ASSETS # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/doc/api/commits.md b/doc/api/commits.md index b41409b4b92..3927a4bbc62 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -11,12 +11,13 @@ GET /projects/:id/repository/commits | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | +| `ref_name` | string | no | The name of a repository branch, tag or revision range, or if not given the default branch | | `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `path` | string | no | The file path | | `all` | boolean | no | Retrieve every commit from the repository | | `with_stats` | boolean | no | Stats about each commit will be added to the response | +| `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits" diff --git a/lib/api/commits.rb b/lib/api/commits.rb index a2f3e87ebd2..ffff40141de 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -37,6 +37,7 @@ module API optional :path, type: String, desc: 'The file path' optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' + optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' use :pagination end get ':id/repository/commits' do @@ -47,6 +48,7 @@ module API offset = (params[:page] - 1) * params[:per_page] all = params[:all] with_stats = params[:with_stats] + first_parent = params[:first_parent] commits = user_project.repository.commits(ref, path: path, @@ -54,11 +56,12 @@ module API offset: offset, before: before, after: after, - all: all) + all: all, + first_parent: first_parent) commit_count = - if all || path || before || after - user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all) + if all || path || before || after || first_parent + user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent) else # Cacheable commit count. user_project.repository.commit_count_for_ref(ref) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 9d4c04b0591..2eaf52355dd 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -140,7 +140,8 @@ module Gitlab request = Gitaly::CountCommitsRequest.new( repository: @gitaly_repo, revision: encode_binary(ref), - all: !!options[:all] + all: !!options[:all], + first_parent: !!options[:first_parent] ) request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present? request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present? @@ -325,6 +326,7 @@ module Gitlab follow: options[:follow], skip_merges: options[:skip_merges], all: !!options[:all], + first_parent: !!options[:first_parent], disable_walk: true # This option is deprecated. The 'walk' implementation is being removed. ) request.after = GitalyClient.timestamp(options[:after]) if options[:after] diff --git a/package.json b/package.json index 3e9b7fca773..9e1ea86b692 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "eslint-plugin-import": "^2.14.0", "eslint-plugin-jasmine": "^2.10.1", "eslint-plugin-jest": "^22.3.0", + "eslint-plugin-no-jquery": "^2.1.0", "gettext-extractor": "^3.4.3", "gettext-extractor-vue": "^4.0.2", "graphql-tag": "^2.10.0", diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js index dfc068ab6ea..964f8b8787e 100644 --- a/spec/frontend/tracking_spec.js +++ b/spec/frontend/tracking_spec.js @@ -1,10 +1,9 @@ -import $ from 'jquery'; import { setHTMLFixture } from './helpers/fixtures'; - import Tracking, { initUserTracking } from '~/tracking'; describe('Tracking', () => { let snowplowSpy; + let bindDocumentSpy; beforeEach(() => { window.snowplow = window.snowplow || (() => {}); @@ -17,6 +16,10 @@ describe('Tracking', () => { }); describe('initUserTracking', () => { + beforeEach(() => { + bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null); + }); + it('calls through to get a new tracker with the expected options', () => { initUserTracking(); expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', { @@ -50,6 +53,11 @@ describe('Tracking', () => { expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking'); }); + + it('binds the document event handling', () => { + initUserTracking(); + expect(bindDocumentSpy).toHaveBeenCalled(); + }); }); describe('.event', () => { @@ -62,11 +70,15 @@ describe('Tracking', () => { it('tracks to snowplow (our current tracking system)', () => { Tracking.event('_category_', '_eventName_', { label: '_label_' }); - expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', { - label: '_label_', - property: '', - value: '', - }); + expect(snowplowSpy).toHaveBeenCalledWith( + 'trackStructEvent', + '_category_', + '_eventName_', + '_label_', + undefined, + undefined, + undefined, + ); }); it('skips tracking if snowplow is unavailable', () => { @@ -99,83 +111,70 @@ describe('Tracking', () => { }); describe('tracking interface events', () => { - let eventSpy = null; - let subject = null; + let eventSpy; + + const trigger = (selector, eventName = 'click') => { + const event = new Event(eventName, { bubbles: true }); + document.querySelector(selector).dispatchEvent(event); + }; beforeEach(() => { eventSpy = jest.spyOn(Tracking, 'event'); - subject = new Tracking('_category_'); + Tracking.bindDocument('_category_'); // only happens once setHTMLFixture(` <input data-track-event="click_input1" data-track-label="_label_" value="_value_"/> <input data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/> <input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/> <input class="dropdown" data-track-event="toggle_dropdown"/> - <div class="js-projects-list-holder"></div> + <div data-track-event="nested_event"><span class="nested"></span></div> `); }); it('binds to clicks on elements matching [data-track-event]', () => { - subject.bind(document); - $('[data-track-event="click_input1"]').click(); + trigger('[data-track-event="click_input1"]'); expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', { label: '_label_', value: '_value_', - property: '', }); }); it('allows value override with the data-track-value attribute', () => { - subject.bind(document); - $('[data-track-event="click_input2"]').click(); + trigger('[data-track-event="click_input2"]'); expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', { - label: '', value: '_value_override_', - property: '', }); }); it('handles checkbox values correctly', () => { - subject.bind(document); - const $checkbox = $('[data-track-event="toggle_checkbox"]'); - - $checkbox.click(); // unchecking + trigger('[data-track-event="toggle_checkbox"]'); // checking expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { - label: '', - property: '', value: false, }); - $checkbox.click(); // checking + trigger('[data-track-event="toggle_checkbox"]'); // unchecking expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { - label: '', - property: '', value: '_value_', }); }); it('handles bootstrap dropdowns', () => { - new Tracking('_category_').bind(document); - const $dropdown = $('[data-track-event="toggle_dropdown"]'); + trigger('[data-track-event="toggle_dropdown"]', 'show.bs.dropdown'); // showing - $dropdown.trigger('show.bs.dropdown'); // showing + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {}); - expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', { - label: '', - property: '', - value: '', - }); + trigger('[data-track-event="toggle_dropdown"]', 'hide.bs.dropdown'); // hiding + + expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {}); + }); - $dropdown.trigger('hide.bs.dropdown'); // hiding + it('handles nested elements inside an element with tracking', () => { + trigger('span.nested', 'click'); - expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', { - label: '', - property: '', - value: '', - }); + expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {}); }); }); }); diff --git a/spec/javascripts/helpers/tracking_helper.js b/spec/javascripts/helpers/tracking_helper.js new file mode 100644 index 00000000000..68c1bd2dbca --- /dev/null +++ b/spec/javascripts/helpers/tracking_helper.js @@ -0,0 +1,25 @@ +import Tracking from '~/tracking'; + +export default Tracking; + +let document; +let handlers; + +export function mockTracking(category = '_category_', documentOverride, spyMethod) { + document = documentOverride || window.document; + window.snowplow = () => {}; + Tracking.bindDocument(category, document); + return spyMethod ? spyMethod(Tracking, 'event') : null; +} + +export function unmockTracking() { + window.snowplow = undefined; + handlers.forEach(event => document.removeEventListener(event.name, event.func)); +} + +export function triggerEvent(selectorOrEl, eventName = 'click') { + const event = new Event(eventName, { bubbles: true }); + const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl; + + el.dispatchEvent(event); +} diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 069e2cb07b5..82d1f815ca8 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -126,6 +126,7 @@ describe('IntegrationSettingsForm', () => { spyOn(axios, 'put').and.callThrough(); integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + // eslint-disable-next-line no-jquery/no-serialize formData = integrationSettingsForm.$form.serialize(); }); diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js index 7fff7c075d9..6c65a55ff29 100644 --- a/spec/javascripts/sidebar/assignee_title_spec.js +++ b/spec/javascripts/sidebar/assignee_title_spec.js @@ -1,13 +1,12 @@ import Vue from 'vue'; import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; +import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper'; describe('AssigneeTitle component', () => { let component; let AssigneeTitleComponent; - let statsSpy; beforeEach(() => { - statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent'); AssigneeTitleComponent = Vue.extend(AssigneeTitle); }); @@ -105,15 +104,20 @@ describe('AssigneeTitle component', () => { expect(component.$el.querySelector('.edit-link')).not.toBeNull(); }); - it('calls trackEvent when edit is clicked', () => { + it('tracks the event when edit is clicked', () => { component = new AssigneeTitleComponent({ propsData: { numberOfAssignees: 0, editable: true, }, }).$mount(); - component.$el.querySelector('.js-sidebar-dropdown-toggle').click(); - expect(statsSpy).toHaveBeenCalled(); + const spy = mockTracking('_category_', component.$el, spyOn); + triggerEvent('.js-sidebar-dropdown-toggle'); + + expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', { + label: 'right_sidebar', + property: 'assignee', + }); }); }); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index ea9e5677bc5..50374b8973f 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -1,13 +1,12 @@ import Vue from 'vue'; import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue'; +import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper'; describe('Confidential Issue Sidebar Block', () => { let vm1; let vm2; - let statsSpy; beforeEach(() => { - statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent'); const Component = Vue.extend(confidentialIssueSidebar); const service = { update: () => Promise.resolve(true), @@ -70,9 +69,13 @@ describe('Confidential Issue Sidebar Block', () => { }); }); - it('calls trackEvent when "Edit" is clicked', () => { - vm1.$el.querySelector('.confidential-edit').click(); + it('tracks the event when "Edit" is clicked', () => { + const spy = mockTracking('_category_', vm1.$el, spyOn); + triggerEvent('.confidential-edit'); - expect(statsSpy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', { + label: 'right_sidebar', + property: 'confidentiality', + }); }); }); diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js index 2d930428230..decccbb8964 100644 --- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js @@ -1,13 +1,12 @@ import Vue from 'vue'; import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue'; +import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper'; describe('LockIssueSidebar', () => { let vm1; let vm2; - let statsSpy; beforeEach(() => { - statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent'); const Component = Vue.extend(lockIssueSidebar); const mediator = { @@ -61,10 +60,14 @@ describe('LockIssueSidebar', () => { }); }); - it('calls trackEvent when "Edit" is clicked', () => { - vm1.$el.querySelector('.lock-edit').click(); + it('tracks an event when "Edit" is clicked', () => { + const spy = mockTracking('_category_', vm1.$el, spyOn); + triggerEvent('.lock-edit'); - expect(statsSpy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', { + label: 'right_sidebar', + property: 'lock_issue', + }); }); it('displays the edit form when opened from collapsed state', done => { diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 2efa13f3fe8..a97608d6b8a 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -2,14 +2,13 @@ import Vue from 'vue'; import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; import eventHub from '~/sidebar/event_hub'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { mockTracking } from 'spec/helpers/tracking_helper'; describe('Subscriptions', function() { let vm; let Subscriptions; - let statsSpy; beforeEach(() => { - statsSpy = spyOnDependency(subscriptions, 'trackEvent'); Subscriptions = Vue.extend(subscriptions); }); @@ -53,6 +52,7 @@ describe('Subscriptions', function() { vm = mountComponent(Subscriptions, { subscribed: true }); spyOn(eventHub, '$emit'); spyOn(vm, '$emit'); + spyOn(vm, 'track'); vm.toggleSubscription(); @@ -60,11 +60,12 @@ describe('Subscriptions', function() { expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object)); }); - it('calls trackEvent when toggled', () => { + it('tracks the event when toggled', () => { vm = mountComponent(Subscriptions, { subscribed: true }); + const spy = mockTracking('_category_', vm.$el, spyOn); vm.toggleSubscription(); - expect(statsSpy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); }); it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => { diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 6dc47e0e501..011b46c7f1a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -279,7 +279,7 @@ describe Repository do describe '#commits' do context 'when neither the all flag nor a ref are specified' do it 'returns every commit from default branch' do - expect(repository.commits(limit: 60).size).to eq(37) + expect(repository.commits(nil, limit: 60).size).to eq(37) end end @@ -320,7 +320,7 @@ describe Repository do context "when 'all' flag is set" do it 'returns every commit from the repository' do - expect(repository.commits(all: true, limit: 60).size).to eq(60) + expect(repository.commits(nil, all: true, limit: 60).size).to eq(60) end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5e6ff40e8cf..90ff1d12bf1 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -169,6 +169,18 @@ describe API::Commits do end end + context 'first_parent optional parameter' do + it 'returns all first_parent commits' do + commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true) + + get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' } + + expect(response).to include_pagination_headers + expect(commit_count).to eq(12) + expect(response.headers['X-Total']).to eq(commit_count.to_s) + end + end + context 'with_stats optional parameter' do let(:project) { create(:project, :public, :repository) } diff --git a/yarn.lock b/yarn.lock index 923f0305ea4..8e6fd5b1a2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4624,6 +4624,11 @@ eslint-plugin-jest@^22.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2" integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA== +eslint-plugin-no-jquery@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.1.0.tgz#d03b74224c5cfbc7fc0bdd12ce4eb400d09e0c0b" + integrity sha512-5sr5tOJRfuRviyAvFTe/mr80TXWxTteD/JHRuJtDN8q/bxAh16eSKoKLAevLC7wZCRN2iwnEfhQPQV4rp/gYtg== + eslint-plugin-promise@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" |