Commit b35b9ac7 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 81f7adf0
...@@ -10,6 +10,7 @@ plugins: ...@@ -10,6 +10,7 @@ plugins:
- import - import
- "@gitlab/i18n" - "@gitlab/i18n"
- "@gitlab/vue-i18n" - "@gitlab/vue-i18n"
- no-jquery
settings: settings:
import/resolver: import/resolver:
webpack: webpack:
...@@ -36,6 +37,11 @@ rules: ...@@ -36,6 +37,11 @@ rules:
vue/no-use-v-if-with-v-for: off vue/no-use-v-if-with-v-for: off
vue/no-v-html: off vue/no-v-html: off
vue/use-v-on-exact: 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: overrides:
files: files:
- '**/spec/**/*' - '**/spec/**/*'
......
...@@ -25,7 +25,7 @@ Then leave running while monitoring and performing some testing through web, api ...@@ -25,7 +25,7 @@ Then leave running while monitoring and performing some testing through web, api
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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 ## 2. Staging Trial
...@@ -41,7 +41,7 @@ Then leave running while monitoring for at least **15 minutes** while performing ...@@ -41,7 +41,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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 ## 4. Production Server Version Check
...@@ -57,7 +57,7 @@ Then leave running while monitoring for at least **15 minutes** while performing ...@@ -57,7 +57,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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 ## 6. Low Impact Check
...@@ -69,7 +69,7 @@ Then leave running while monitoring for at least **30 minutes** while performing ...@@ -69,7 +69,7 @@ Then leave running while monitoring for at least **30 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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 ## 7. Mid Impact Trial
...@@ -81,7 +81,7 @@ Then leave running while monitoring for at least **12 hours** while performing s ...@@ -81,7 +81,7 @@ Then leave running while monitoring for at least **12 hours** while performing s
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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 ## 8. Full Impact Trial
...@@ -93,7 +93,7 @@ Then leave running while monitoring for at least **1 week**. ...@@ -93,7 +93,7 @@ Then leave running while monitoring for at least **1 week**.
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) - [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) - [ ] [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? #### Success?
......
...@@ -446,7 +446,7 @@ group :ed25519 do ...@@ -446,7 +446,7 @@ group :ed25519 do
end end
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.58.0' gem 'gitaly', '~> 1.65.0'
gem 'grpc', '~> 1.19.0' gem 'grpc', '~> 1.19.0'
......
...@@ -358,7 +358,7 @@ GEM ...@@ -358,7 +358,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
git (1.5.0) git (1.5.0)
gitaly (1.58.0) gitaly (1.65.0)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-labkit (0.5.2) gitlab-labkit (0.5.2)
...@@ -1168,7 +1168,7 @@ DEPENDENCIES ...@@ -1168,7 +1168,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.58.0) gitaly (~> 1.65.0)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5) gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
export const initSidebarTracking = () => {};
export const trackEvent = () => {};
// Noop function which has a EE counter-part
export default () => {};
...@@ -22,6 +22,7 @@ export default class FilterableList { ...@@ -22,6 +22,7 @@ export default class FilterableList {
getPagePath() { getPagePath() {
const action = this.filterForm.getAttribute('action'); const action = this.filterForm.getAttribute('action');
// eslint-disable-next-line no-jquery/no-serialize
const params = $(this.filterForm).serialize(); const params = $(this.filterForm).serialize();
return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`; return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`;
} }
......
...@@ -42,6 +42,7 @@ export default class IntegrationSettingsForm { ...@@ -42,6 +42,7 @@ export default class IntegrationSettingsForm {
// and test the service using provided configuration. // and test the service using provided configuration.
if (this.$form.get(0).checkValidity() && this.canTestService) { if (this.$form.get(0).checkValidity() && this.canTestService) {
e.preventDefault(); e.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
this.testSettings(this.$form.serialize()); this.testSettings(this.$form.serialize());
} }
} }
......
import Vue from 'vue'; import Vue from 'vue';
import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data'; import { parseIssuableData } from './utils/parse_data';
...@@ -9,9 +8,6 @@ export default function initIssueableApp() { ...@@ -9,9 +8,6 @@ export default function initIssueableApp() {
components: { components: {
issuableApp, issuableApp,
}, },
mounted() {
initSidebarTracking();
},
render(createElement) { render(createElement) {
return createElement('issuable-app', { return createElement('issuable-app', {
props: parseIssuableData(), props: parseIssuableData(),
......
...@@ -314,6 +314,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -314,6 +314,7 @@ document.addEventListener('DOMContentLoaded', () => {
const action = `${this.action}${link.search === '' ? '?' : '&'}`; const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault(); event.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
visitUrl(`${action}${$(this).serialize()}`); visitUrl(`${action}${$(this).serialize()}`);
}); });
......
...@@ -1461,6 +1461,7 @@ export default class Notes { ...@@ -1461,6 +1461,7 @@ export default class Notes {
getFormData($form) { getFormData($form) {
const content = $form.find('.js-note-text').val(); const content = $form.find('.js-note-text').val();
return { return {
// eslint-disable-next-line no-jquery/no-serialize
formData: $form.serialize(), formData: $form.serialize(),
formContent: _.escape(content), formContent: _.escape(content),
formAction: $form.attr('action'), formAction: $form.attr('action'),
......
...@@ -19,7 +19,9 @@ export default { ...@@ -19,7 +19,9 @@ export default {
<gl-button <gl-button
ref="button" ref="button"
v-gl-tooltip 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" variant="transparent"
:title="__('Reply to comment')" :title="__('Reply to comment')"
@click="$emit('startReplying')" @click="$emit('startReplying')"
......
import Vue from 'vue'; import Vue from 'vue';
import initNoteStats from 'ee_else_ce/event_tracking/notes';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters'; import initDiscussionFilters from './discussion_filters';
import createStore from './stores'; import createStore from './stores';
...@@ -39,9 +38,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -39,9 +38,6 @@ document.addEventListener('DOMContentLoaded', () => {
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
}, },
mounted() {
initNoteStats();
},
render(createElement) { render(createElement) {
return createElement('notes-app', { return createElement('notes-app', {
props: { props: {
......
...@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({ new MiniPipelineGraph({
container: '.js-commit-pipeline-graph', container: '.js-commit-pipeline-graph',
}).bindEvents(); }).bindEvents();
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines(); initPipelines();
}); });
...@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
}).bindEvents(); }).bindEvents();
initNotes(); initNotes();
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); 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); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests(); fetchCommitMergeRequests();
initDiffNotes(); initDiffNotes();
......
<script> <script>
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
name: 'AssigneeTitle', name: 'AssigneeTitle',
...@@ -30,11 +29,6 @@ export default { ...@@ -30,11 +29,6 @@ export default {
return n__('Assignee', `%d Assignees`, assignees); return n__('Assignee', `%d Assignees`, assignees);
}, },
}, },
methods: {
trackEdit() {
trackEvent('click_edit_button', 'assignee');
},
},
}; };
</script> </script>
<template> <template>
...@@ -45,7 +39,9 @@ export default { ...@@ -45,7 +39,9 @@ export default {
v-if="editable" v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right" class="js-sidebar-dropdown-toggle edit-link float-right"
href="#" href="#"
@click.prevent="trackEdit" data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
......
...@@ -5,7 +5,6 @@ import tooltip from '~/vue_shared/directives/tooltip'; ...@@ -5,7 +5,6 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
components: { components: {
...@@ -52,11 +51,6 @@ export default { ...@@ -52,11 +51,6 @@ export default {
toggleForm() { toggleForm() {
this.edit = !this.edit; this.edit = !this.edit;
}, },
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'confidentiality');
},
updateConfidentialAttribute(confidential) { updateConfidentialAttribute(confidential) {
this.service this.service
.update('issue', { confidential }) .update('issue', { confidential })
...@@ -88,7 +82,10 @@ export default { ...@@ -88,7 +82,10 @@ export default {
v-if="isEditable" v-if="isEditable"
class="float-right confidential-edit" class="float-right confidential-edit"
href="#" href="#"
@click.prevent="onEditClick" data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
......
...@@ -6,7 +6,6 @@ import issuableMixin from '~/vue_shared/mixins/issuable'; ...@@ -6,7 +6,6 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default { export default {
components: { components: {
...@@ -66,11 +65,6 @@ export default { ...@@ -66,11 +65,6 @@ export default {
toggleForm() { toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
}, },
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'lock_issue');
},
updateLockedAttribute(locked) { updateLockedAttribute(locked) {
this.mediator.service this.mediator.service
.update(this.issuableType, { .update(this.issuableType, {
...@@ -114,7 +108,10 @@ export default { ...@@ -114,7 +108,10 @@ export default {
v-if="isEditable" v-if="isEditable"
class="float-right lock-edit" class="float-right lock-edit"
type="button" 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') }} {{ __('Edit') }}
</button> </button>
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import Tracking from '~/tracking';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications'; const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off'; const ICON_OFF = 'notifications-off';
...@@ -19,6 +19,7 @@ export default { ...@@ -19,6 +19,7 @@ export default {
icon, icon,
toggleButton, toggleButton,
}, },
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
...@@ -65,7 +66,10 @@ export default { ...@@ -65,7 +66,10 @@ export default {
// Component event emission. // Component event emission.
this.$emit('toggleSubscription', this.id); 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() { onClickCollapsedIcon() {
this.$emit('toggleSidebar'); this.$emit('toggleSidebar');
......
import $ from 'jquery'; import _ from 'underscore';
const DEFAULT_SNOWPLOW_OPTIONS = { const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl', namespace: 'gl',
...@@ -14,18 +14,31 @@ const DEFAULT_SNOWPLOW_OPTIONS = { ...@@ -14,18 +14,31 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
linkClickTracking: false, linkClickTracking: false,
}; };
const extractData = (el, opts = {}) => { const eventHandler = (e, func, opts = {}) => {
const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset; const el = e.target.closest('[data-track-event]');
let trackValue = el.dataset.trackValue || el.value || ''; const action = el && el.dataset.trackEvent;
if (el.type === 'checkbox' && !el.checked) trackValue = false; if (!action) return;
return [
trackEvent + (opts.suffix || ''), let value = el.dataset.trackValue || el.value || undefined;
{ if (el.type === 'checkbox' && !el.checked) value = false;
label: trackLabel,
property: trackProperty, const data = {
value: trackValue, 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 { export default class Tracking {
...@@ -39,49 +52,43 @@ export default class Tracking { ...@@ -39,49 +52,43 @@ export default class Tracking {
return typeof window.snowplow === 'function' && this.trackable(); 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; if (!this.enabled()) return false;
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.'); if (!category) throw new Error('Tracking: no category provided for tracking.');
return window.snowplow( const { label, property, value, context } = data;
'trackStructEvent', const contexts = context ? [context] : undefined;
category, return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
event,
Object.assign({}, { label: '', property: '', value: '' }, data),
);
} }
constructor(category = document.body.dataset.page) { static bindDocument(category = document.body.dataset.page, documentOverride = null) {
this.category = category; const el = documentOverride || document;
} if (!this.enabled() || el.trackingBound) return [];
bind(container = document) { el.trackingBound = true;
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));
});
}
customHandlingFor(el) { const handlers = eventHandlers(category, (...args) => this.event(...args));
const classes = el.classList; handlers.forEach(event => el.addEventListener(event.name, event.func));
return handlers;
// 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;
} }
eventHandler(category = null, opts = {}) { static mixin(opts) {
return e => { return {
this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts)); 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 { ...@@ -89,7 +96,7 @@ export default class Tracking {
export function initUserTracking() { export function initUserTracking() {
if (!Tracking.enabled()) return; 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('newTracker', opts.namespace, opts.hostname, opts);
window.snowplow('enableActivityTracking', 30, 30); window.snowplow('enableActivityTracking', 30, 30);
...@@ -97,4 +104,6 @@ export function initUserTracking() { ...@@ -97,4 +104,6 @@ export function initUserTracking() {
if (opts.formTracking) window.snowplow('enableFormTracking'); if (opts.formTracking) window.snowplow('enableFormTracking');
if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
} }
...@@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); ...@@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: $flash-container-top; top: $flash-container-top;
z-index: 200; z-index: 251;
.flash-content { .flash-content {
box-shadow: 0 2px 4px 0 $notification-box-shadow-color; box-shadow: 0 2px 4px 0 $notification-box-shadow-color;
......
...@@ -133,18 +133,28 @@ class Repository ...@@ -133,18 +133,28 @@ class Repository
end end
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 = { options = {
repo: raw_repository, repo: raw_repository,
ref: ref, ref: ref,
path: path, path: opts[:path],
limit: limit, follow: Array(opts[:path]).length == 1,
offset: offset, limit: opts[:limit],
after: after, offset: opts[:offset],
before: before, skip_merges: !!opts[:skip_merges],
follow: Array(path).length == 1, after: opts[:after],
skip_merges: skip_merges, before: opts[:before],
all: all all: !!opts[:all],
first_parent: !!opts[:first_parent]
} }
commits = Gitlab::Git::Commit.where(options) commits = Gitlab::Git::Commit.where(options)
......
---
title: Add first_parent option to list commits api
merge_request: 32410
author: jhenkens
type: added
...@@ -13,6 +13,7 @@ Bundler.require(*Rails.groups) ...@@ -13,6 +13,7 @@ Bundler.require(*Rails.groups)
module Gitlab module Gitlab
class Application < Rails::Application class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab') 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/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/queues')
...@@ -46,9 +47,10 @@ module Gitlab ...@@ -46,9 +47,10 @@ module Gitlab
config.generators.templates.push("#{config.root}/generator_templates") config.generators.templates.push("#{config.root}/generator_templates")
if Gitlab.ee?
ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo| 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)) ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root))
memo << ee_path.to_s if ee_path.exist? memo << ee_path.to_s
end end
# Eager load should load CE first # Eager load should load CE first
...@@ -58,6 +60,7 @@ module Gitlab ...@@ -58,6 +60,7 @@ module Gitlab
# Other than Ruby modules we load EE first # Other than Ruby modules we load EE first
config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks" config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks"
config.paths['app/views'].unshift "#{config.root}/ee/app/views" config.paths['app/views'].unshift "#{config.root}/ee/app/views"
end
# Rake tasks ignore the eager loading settings, so we need to set the # Rake tasks ignore the eager loading settings, so we need to set the
# autoload paths explicitly # autoload paths explicitly
...@@ -178,16 +181,18 @@ module Gitlab ...@@ -178,16 +181,18 @@ module Gitlab
config.assets.paths << "#{config.root}/node_modules/xterm/src/" config.assets.paths << "#{config.root}/node_modules/xterm/src/"
config.assets.precompile << "xterm.css" config.assets.precompile << "xterm.css"
if Gitlab.ee?
%w[images javascripts stylesheets].each do |path| %w[images javascripts stylesheets].each do |path|
config.assets.paths << "#{config.root}/ee/app/assets/#{path}" config.assets.paths << "#{config.root}/ee/app/assets/#{path}"
config.assets.precompile << "jira_connect.js" config.assets.precompile << "jira_connect.js"
config.assets.precompile << "pages/jira_connect.css" config.assets.precompile << "pages/jira_connect.css"
end end
end
# Import path for EE specific SCSS entry point # Import path for EE specific SCSS entry point
# In CE it will import a noop file, in EE a functioning file # In CE it will import a noop file, in EE a functioning file
# Order is important, so that the ee file takes precedence: # 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}/app/assets/stylesheets/_ee"
config.assets.paths << "#{config.root}/vendor/assets/javascripts/" config.assets.paths << "#{config.root}/vendor/assets/javascripts/"
...@@ -197,6 +202,7 @@ module Gitlab ...@@ -197,6 +202,7 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508 # See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508
config.assets.paths << "#{config.root}/node_modules" config.assets.paths << "#{config.root}/node_modules"
if Gitlab.ee?
# Compile non-JS/CSS assets in the ee/app/assets folder by default # 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 # 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| LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename|
...@@ -204,6 +210,7 @@ module Gitlab ...@@ -204,6 +210,7 @@ module Gitlab
!['.js', '.css', ''].include?(File.extname(logical_path)) !['.js', '.css', ''].include?(File.extname(logical_path))
end end
config.assets.precompile << LOOSE_EE_APP_ASSETS config.assets.precompile << LOOSE_EE_APP_ASSETS
end
# Version of your assets, change this if you want to expire all your assets # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
...@@ -11,12 +11,13 @@ GET /projects/:id/repository/commits ...@@ -11,12 +11,13 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description | | 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 | `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 | | `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 | | `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 | | `path` | string | no | The file path |
| `all` | boolean | no | Retrieve every commit from the repository | | `all` | boolean | no | Retrieve every commit from the repository |
| `with_stats` | boolean | no | Stats about each commit will be added to the response | | `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 ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits" curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
......
...@@ -37,6 +37,7 @@ module API ...@@ -37,6 +37,7 @@ module API
optional :path, type: String, desc: 'The file path' optional :path, type: String, desc: 'The file path'
optional :all, type: Boolean, desc: 'Every commit will be returned' 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 :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 use :pagination
end end
get ':id/repository/commits' do get ':id/repository/commits' do
...@@ -47,6 +48,7 @@ module API ...@@ -47,6 +48,7 @@ module API
offset = (params[:page] - 1) * params[:per_page] offset = (params[:page] - 1) * params[:per_page]
all = params[:all] all = params[:all]
with_stats = params[:with_stats] with_stats = params[:with_stats]
first_parent = params[:first_parent]
commits = user_project.repository.commits(ref, commits = user_project.repository.commits(ref,
path: path, path: path,
...@@ -54,11 +56,12 @@ module API ...@@ -54,11 +56,12 @@ module API
offset: offset, offset: offset,
before: before, before: before,
after: after, after: after,
all: all) all: all,
first_parent: first_parent)
commit_count = commit_count =
if all || path || before || after if all || path || before || after || first_parent
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all) user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent)
else else
# Cacheable commit count. # Cacheable commit count.
user_project.repository.commit_count_for_ref(ref) user_project.repository.commit_count_for_ref(ref)
......
...@@ -140,7 +140,8 @@ module Gitlab ...@@ -140,7 +140,8 @@ module Gitlab
request = Gitaly::CountCommitsRequest.new( request = Gitaly::CountCommitsRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
revision: encode_binary(ref), 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.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? request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
...@@ -325,6 +326,7 @@ module Gitlab ...@@ -325,6 +326,7 @@ module Gitlab
follow: options[:follow], follow: options[:follow],
skip_merges: options[:skip_merges], skip_merges: options[:skip_merges],
all: !!options[:all], all: !!options[:all],
first_parent: !!options[:first_parent],
disable_walk: true # This option is deprecated. The 'walk' implementation is being removed. disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
) )
request.after = GitalyClient.timestamp(options[:after]) if options[:after] request.after = GitalyClient.timestamp(options[:after]) if options[:after]
......
import $ from 'jquery';
import { setHTMLFixture } from './helpers/fixtures'; import { setHTMLFixture } from './helpers/fixtures';
import Tracking, { initUserTracking } from '~/tracking'; import Tracking, { initUserTracking } from '~/tracking';
describe('Tracking', () => { describe('Tracking', () => {
let snowplowSpy; let snowplowSpy;
let bindDocumentSpy;
beforeEach(() => { beforeEach(() => {
window.snowplow = window.snowplow || (() => {}); window.snowplow = window.snowplow || (() => {});
...@@ -17,6 +16,10 @@ describe('Tracking', () => { ...@@ -17,6 +16,10 @@ describe('Tracking', () => {
}); });
describe('initUserTracking', () => { describe('initUserTracking', () => {
beforeEach(() => {
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
});
it('calls through to get a new tracker with the expected options', () => { it('calls through to get a new tracker with the expected options', () => {
initUserTracking(); initUserTracking();
expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', { expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', {
...@@ -50,6 +53,11 @@ describe('Tracking', () => { ...@@ -50,6 +53,11 @@ describe('Tracking', () => {
expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking'); expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking'); expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
}); });
it('binds the document event handling', () => {
initUserTracking();
expect(bindDocumentSpy).toHaveBeenCalled();
});
}); });
describe('.event', () => { describe('.event', () => {
...@@ -62,11 +70,15 @@ describe('Tracking', () => { ...@@ -62,11 +70,15 @@ describe('Tracking', () => {
it('tracks to snowplow (our current tracking system)', () => { it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' }); Tracking.event('_category_', '_eventName_', { label: '_label_' });
expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', { expect(snowplowSpy).toHaveBeenCalledWith(
label: '_label_', 'trackStructEvent',
property: '', '_category_',
value: '', '_eventName_',
}); '_label_',
undefined,
undefined,
undefined,
);
}); });
it('skips tracking if snowplow is unavailable', () => { it('skips tracking if snowplow is unavailable', () => {
...@@ -99,83 +111,70 @@ describe('Tracking', () => { ...@@ -99,83 +111,70 @@ describe('Tracking', () => {
}); });
describe('tracking interface events', () => { describe('tracking interface events', () => {
let eventSpy = null; let eventSpy;
let subject = null;
const trigger = (selector, eventName = 'click') => {
const event = new Event(eventName, { bubbles: true });
document.querySelector(selector).dispatchEvent(event);
};
beforeEach(() => { beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event'); eventSpy = jest.spyOn(Tracking, 'event');
subject = new Tracking('_category_'); Tracking.bindDocument('_category_'); // only happens once
setHTMLFixture(` setHTMLFixture(`
<input data-track-event="click_input1" data-track-label="_label_" value="_value_"/> <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 data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/>
<input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/> <input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/>
<input class="dropdown" data-track-event="toggle_dropdown"/> <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]', () => { it('binds to clicks on elements matching [data-track-event]', () => {
subject.bind(document); trigger('[data-track-event="click_input1"]');
$('[data-track-event="click_input1"]').click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
label: '_label_', label: '_label_',
value: '_value_', value: '_value_',
property: '',
}); });
}); });
it('allows value override with the data-track-value attribute', () => { it('allows value override with the data-track-value attribute', () => {
subject.bind(document); trigger('[data-track-event="click_input2"]');
$('[data-track-event="click_input2"]').click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
label: '',
value: '_value_override_', value: '_value_override_',
property: '',
}); });
}); });
it('handles checkbox values correctly', () => { it('handles checkbox values correctly', () => {
subject.bind(document); trigger('[data-track-event="toggle_checkbox"]'); // checking
const $checkbox = $('[data-track-event="toggle_checkbox"]');
$checkbox.click(); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: false, value: false,
}); });
$checkbox.click(); // checking trigger('[data-track-event="toggle_checkbox"]'); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: '_value_', value: '_value_',
}); });
}); });
it('handles bootstrap dropdowns', () => { it('handles bootstrap dropdowns', () => {
new Tracking('_category_').bind(document); trigger('[data-track-event="toggle_dropdown"]', 'show.bs.dropdown'); // showing
const $dropdown = $('[data-track-event="toggle_dropdown"]');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {});
$dropdown.trigger('show.bs.dropdown'); // showing trigger('[data-track-event="toggle_dropdown"]', 'hide.bs.dropdown'); // hiding
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {});
label: '',
property: '',
value: '',
}); });
$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', { expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {});
label: '',
property: '',
value: '',
});
}); });
}); });
}); });
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);
}
...@@ -126,6 +126,7 @@ describe('IntegrationSettingsForm', () => { ...@@ -126,6 +126,7 @@ describe('IntegrationSettingsForm', () => {
spyOn(axios, 'put').and.callThrough(); spyOn(axios, 'put').and.callThrough();
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
// eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize(); formData = integrationSettingsForm.$form.serialize();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('AssigneeTitle component', () => { describe('AssigneeTitle component', () => {
let component; let component;
let AssigneeTitleComponent; let AssigneeTitleComponent;
let statsSpy;
beforeEach(() => { beforeEach(() => {
statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent');
AssigneeTitleComponent = Vue.extend(AssigneeTitle); AssigneeTitleComponent = Vue.extend(AssigneeTitle);
}); });
...@@ -105,15 +104,20 @@ describe('AssigneeTitle component', () => { ...@@ -105,15 +104,20 @@ describe('AssigneeTitle component', () => {
expect(component.$el.querySelector('.edit-link')).not.toBeNull(); 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({ component = new AssigneeTitleComponent({
propsData: { propsData: {
numberOfAssignees: 0, numberOfAssignees: 0,
editable: true, editable: true,
}, },
}).$mount(); }).$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',
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue'; import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('Confidential Issue Sidebar Block', () => { describe('Confidential Issue Sidebar Block', () => {
let vm1; let vm1;
let vm2; let vm2;
let statsSpy;
beforeEach(() => { beforeEach(() => {
statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent');
const Component = Vue.extend(confidentialIssueSidebar); const Component = Vue.extend(confidentialIssueSidebar);
const service = { const service = {
update: () => Promise.resolve(true), update: () => Promise.resolve(true),
...@@ -70,9 +69,13 @@ describe('Confidential Issue Sidebar Block', () => { ...@@ -70,9 +69,13 @@ describe('Confidential Issue Sidebar Block', () => {
}); });
}); });
it('calls trackEvent when "Edit" is clicked', () => { it('tracks the event when "Edit" is clicked', () => {
vm1.$el.querySelector('.confidential-edit').click(); 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',
});
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue'; import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('LockIssueSidebar', () => { describe('LockIssueSidebar', () => {
let vm1; let vm1;
let vm2; let vm2;
let statsSpy;
beforeEach(() => { beforeEach(() => {
statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent');
const Component = Vue.extend(lockIssueSidebar); const Component = Vue.extend(lockIssueSidebar);
const mediator = { const mediator = {
...@@ -61,10 +60,14 @@ describe('LockIssueSidebar', () => { ...@@ -61,10 +60,14 @@ describe('LockIssueSidebar', () => {
}); });
}); });
it('calls trackEvent when "Edit" is clicked', () => { it('tracks an event when "Edit" is clicked', () => {
vm1.$el.querySelector('.lock-edit').click(); 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 => { it('displays the edit form when opened from collapsed state', done => {
......
...@@ -2,14 +2,13 @@ import Vue from 'vue'; ...@@ -2,14 +2,13 @@ import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTracking } from 'spec/helpers/tracking_helper';
describe('Subscriptions', function() { describe('Subscriptions', function() {
let vm; let vm;
let Subscriptions; let Subscriptions;
let statsSpy;
beforeEach(() => { beforeEach(() => {
statsSpy = spyOnDependency(subscriptions, 'trackEvent');
Subscriptions = Vue.extend(subscriptions); Subscriptions = Vue.extend(subscriptions);
}); });
...@@ -53,6 +52,7 @@ describe('Subscriptions', function() { ...@@ -53,6 +52,7 @@ describe('Subscriptions', function() {
vm = mountComponent(Subscriptions, { subscribed: true }); vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
spyOn(vm, '$emit'); spyOn(vm, '$emit');
spyOn(vm, 'track');
vm.toggleSubscription(); vm.toggleSubscription();
...@@ -60,11 +60,12 @@ describe('Subscriptions', function() { ...@@ -60,11 +60,12 @@ describe('Subscriptions', function() {
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object)); expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
}); });
it('calls trackEvent when toggled', () => { it('tracks the event when toggled', () => {
vm = mountComponent(Subscriptions, { subscribed: true }); vm = mountComponent(Subscriptions, { subscribed: true });
const spy = mockTracking('_category_', vm.$el, spyOn);
vm.toggleSubscription(); vm.toggleSubscription();
expect(statsSpy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => { it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
......
...@@ -279,7 +279,7 @@ describe Repository do ...@@ -279,7 +279,7 @@ describe Repository do
describe '#commits' do describe '#commits' do
context 'when neither the all flag nor a ref are specified' do context 'when neither the all flag nor a ref are specified' do
it 'returns every commit from default branch' 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
end end
...@@ -320,7 +320,7 @@ describe Repository do ...@@ -320,7 +320,7 @@ describe Repository do
context "when 'all' flag is set" do context "when 'all' flag is set" do
it 'returns every commit from the repository' 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 end
end end
......
...@@ -169,6 +169,18 @@ describe API::Commits do ...@@ -169,6 +169,18 @@ describe API::Commits do
end end
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 context 'with_stats optional parameter' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
......
...@@ -4624,6 +4624,11 @@ eslint-plugin-jest@^22.3.0: ...@@ -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" resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA== 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: eslint-plugin-promise@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment