Commit fb462e2e authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-05-10

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 47149736 2b778174
...@@ -79,7 +79,7 @@ stages: ...@@ -79,7 +79,7 @@ stages:
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing # https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs: &except-docs .except-docs: &except-docs
except: except:
- /^docs\/.*/ - /(^docs[\/-].*|.*-docs$)/
.rspec-knapsack: &rspec-knapsack .rspec-knapsack: &rspec-knapsack
stage: test stage: test
...@@ -313,7 +313,7 @@ downtime_check: ...@@ -313,7 +313,7 @@ downtime_check:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?$/
- /^docs\/*/ - /(^docs[\/-].*|.*-docs$)/
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
stage: test stage: test
......
...@@ -120,7 +120,7 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -120,7 +120,7 @@ const DiffNoteAvatars = Vue.extend({
}, },
methods: { methods: {
clickedAvatar(e) { clickedAvatar(e) {
notes.addDiffNote(e); notes.onAddDiffNote(e);
// Toggle the active state of the toggle all button // Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState(); this.toggleDiscussionsToggleState();
......
...@@ -57,6 +57,7 @@ import BlobViewer from './blob/viewer/index'; ...@@ -57,6 +57,7 @@ import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes'; import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root'; import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import GfmAutoComplete from './gfm_auto_complete';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -83,6 +84,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -83,6 +84,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
function initBlob() { function initBlob() {
new LineHighlighter(); new LineHighlighter();
......
...@@ -20,10 +20,14 @@ class FilteredSearchManager { ...@@ -20,10 +20,14 @@ class FilteredSearchManager {
this.recentSearchesStore = new RecentSearchesStore({ this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(), isLocalStorageAvailable: RecentSearchesService.isAvailable(),
}); });
let recentSearchesKey = 'issue-recent-searches'; const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown');
const projectPath = searchHistoryDropdownElement ?
searchHistoryDropdownElement.dataset.projectFullPath : 'project';
let recentSearchesPagePrefix = 'issue-recent-searches';
if (page === 'merge_requests') { if (page === 'merge_requests') {
recentSearchesKey = 'merge-request-recent-searches'; recentSearchesPagePrefix = 'merge-request-recent-searches';
} }
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey); this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage // Fetch recent searches from localStorage
...@@ -51,7 +55,7 @@ class FilteredSearchManager { ...@@ -51,7 +55,7 @@ class FilteredSearchManager {
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
this.recentSearchesService, this.recentSearchesService,
document.querySelector('.js-filtered-search-history-dropdown'), searchHistoryDropdownElement,
); );
this.recentSearchesRoot.init(); this.recentSearchesRoot.init();
......
...@@ -3,6 +3,7 @@ import _ from 'underscore'; ...@@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore { class RecentSearchesStore {
constructor(initialState = {}) { constructor(initialState = {}) {
this.state = Object.assign({ this.state = Object.assign({
isLocalStorageAvailable: true,
recentSearches: [], recentSearches: [],
}, initialState); }, initialState);
} }
......
This diff is collapsed.
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
/* global DropzoneInput */ /* global DropzoneInput */
/* global autosize */ /* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {}; window.gl = window.gl || {};
function GLForm(form) { function GLForm(form) {
...@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() { ...@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() {
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form); new DropzoneInput(this.form);
autosize(this.textarea); autosize(this.textarea);
} }
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */ /* global Pikaday */
import GfmAutoComplete from './gfm_auto_complete';
(function() { (function() {
this.IssuableForm = (function() { this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
...@@ -20,7 +22,7 @@ ...@@ -20,7 +22,7 @@
this.renderWipExplanation = this.renderWipExplanation.bind(this); this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this); this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
gl.GfmAutoComplete.setup(); new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect(); new UsersSelect();
new GroupsSelect(); new GroupsSelect();
new ZenMode(); new ZenMode();
......
...@@ -97,7 +97,6 @@ import './dropzone_input'; ...@@ -97,7 +97,6 @@ import './dropzone_input';
import './due_date_select'; import './due_date_select';
import './files_comment_button'; import './files_comment_button';
import './flash'; import './flash';
import './gfm_auto_complete';
import './gl_dropdown'; import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
......
/* eslint-disable no-new, class-methods-use-this */ /* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */ /* global Breakpoints */
/* global Flash */ /* global Flash */
/* global notes */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import './breakpoints'; import './breakpoints';
...@@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.ajaxGet({ this.ajaxGet({
url: `${urlPathname}.json${location.search}`, url: `${urlPathname}.json${location.search}`,
success: (data) => { success: (data) => {
$('#diffs').html(data.html); const $container = $('#diffs');
$container.html(data.html);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
...@@ -278,6 +280,20 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -278,6 +280,20 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
}) })
.init(); .init();
}); });
// Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
if (anchor) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.addDiffNote(anchor, lineType, false);
anchor[0].scrollIntoView();
// We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
}
}, },
}); });
} }
......
...@@ -12,7 +12,6 @@ require('./autosave'); ...@@ -12,7 +12,6 @@ require('./autosave');
window.autosize = require('vendor/autosize'); window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone'); window.Dropzone = require('dropzone');
require('./dropzone_input'); require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho'); require('vendor/jquery.atwho');
require('./task_list'); require('./task_list');
...@@ -24,7 +23,7 @@ const normalizeNewlines = function(str) { ...@@ -24,7 +23,7 @@ const normalizeNewlines = function(str) {
(function() { (function() {
this.Notes = (function() { this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_SLASH_COMMANDS = /^\/\w+/gm; const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm;
Notes.interval = null; Notes.interval = null;
...@@ -33,9 +32,9 @@ const normalizeNewlines = function(str) { ...@@ -33,9 +32,9 @@ const normalizeNewlines = function(str) {
this.updateComment = this.updateComment.bind(this); this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.bind(this); this.visibilityChange = this.visibilityChange.bind(this);
this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
this.addDiffNote = this.addDiffNote.bind(this); this.onAddDiffNote = this.onAddDiffNote.bind(this);
this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
this.replyToDiscussionNote = this.replyToDiscussionNote.bind(this); this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this); this.removeNote = this.removeNote.bind(this);
this.cancelEdit = this.cancelEdit.bind(this); this.cancelEdit = this.cancelEdit.bind(this);
this.updateNote = this.updateNote.bind(this); this.updateNote = this.updateNote.bind(this);
...@@ -100,9 +99,9 @@ const normalizeNewlines = function(str) { ...@@ -100,9 +99,9 @@ const normalizeNewlines = function(str) {
// update the file name when an attachment is selected // update the file name when an attachment is selected
$(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
// reply to diff/discussion notes // reply to diff/discussion notes
$(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote);
// add diff note // add diff note
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote); $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote);
// hide diff note form // hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
// toggle commit list // toggle commit list
...@@ -794,10 +793,14 @@ const normalizeNewlines = function(str) { ...@@ -794,10 +793,14 @@ const normalizeNewlines = function(str) {
Shows the note form below the notes. Shows the note form below the notes.
*/ */
Notes.prototype.replyToDiscussionNote = function(e) { Notes.prototype.onReplyToDiscussionNote = function(e) {
this.replyToDiscussionNote(e.target);
};
Notes.prototype.replyToDiscussionNote = function(target) {
var form, replyLink; var form, replyLink;
form = this.cleanForm(this.formClone.clone()); form = this.cleanForm(this.formClone.clone());
replyLink = $(e.target).closest(".js-discussion-reply-button"); replyLink = $(target).closest(".js-discussion-reply-button");
// insert the form after the button // insert the form after the button
replyLink replyLink
.closest('.discussion-reply-holder') .closest('.discussion-reply-holder')
...@@ -867,35 +870,43 @@ const normalizeNewlines = function(str) { ...@@ -867,35 +870,43 @@ const normalizeNewlines = function(str) {
Sets up the form and shows it. Sets up the form and shows it.
*/ */
Notes.prototype.addDiffNote = function(e) { Notes.prototype.onAddDiffNote = function(e) {
var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
e.preventDefault(); e.preventDefault();
$link = $(e.currentTarget || e.target); const $link = $(e.currentTarget || e.target);
const showReplyInput = !$link.hasClass('js-diff-comment-avatar');
this.addDiffNote($link, $link.data('lineType'), showReplyInput);
};
Notes.prototype.addDiffNote = function(target, lineType, showReplyInput) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
$link = $(target);
row = $link.closest("tr"); row = $link.closest("tr");
nextRow = row.next(); const nextRow = row.next();
hasNotes = nextRow.is(".notes_holder"); let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
hasNotes = targetRow.is(".notes_holder");
addForm = false; addForm = false;
notesContentSelector = ".notes_content"; let lineTypeSelector = '';
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) { if (this.isParallelView()) {
lineType = $link.data("lineType"); lineTypeSelector = `.${lineType}`;
notesContentSelector += "." + lineType;
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
} }
notesContentSelector += " .content"; const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
notesContent = nextRow.find(notesContentSelector); let notesContent = targetRow.find(notesContentSelector);
if (hasNotes && !isDiffCommentAvatar) { if (hasNotes && showReplyInput) {
nextRow.show(); targetRow.show();
notesContent = nextRow.find(notesContentSelector); notesContent = targetRow.find(notesContentSelector);
if (notesContent.length) { if (notesContent.length) {
notesContent.show(); notesContent.show();
replyButton = notesContent.find(".js-discussion-reply-button:visible"); replyButton = notesContent.find(".js-discussion-reply-button:visible");
if (replyButton.length) { if (replyButton.length) {
e.target = replyButton[0]; this.replyToDiscussionNote(replyButton[0]);
$.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
} else { } else {
// In parallel view, the form may not be present in one of the panes // In parallel view, the form may not be present in one of the panes
noteForm = notesContent.find(".js-discussion-note-form"); noteForm = notesContent.find(".js-discussion-note-form");
...@@ -904,18 +915,18 @@ const normalizeNewlines = function(str) { ...@@ -904,18 +915,18 @@ const normalizeNewlines = function(str) {
} }
} }
} }
} else if (!isDiffCommentAvatar) { } else if (showReplyInput) {
// add a notes row and insert the form // add a notes row and insert the form
row.after(rowCssToAdd); row.after(rowCssToAdd);
nextRow = row.next(); targetRow = row.next();
notesContent = nextRow.find(notesContentSelector); notesContent = targetRow.find(notesContentSelector);
addForm = true; addForm = true;
} else { } else {
nextRow.show(); targetRow.show();
notesContent.toggle(!notesContent.is(':visible')); notesContent.toggle(!notesContent.is(':visible'));
if (!nextRow.find('.content:not(:empty)').is(':visible')) { if (!targetRow.find('.content:not(:empty)').is(':visible')) {
nextRow.hide(); targetRow.hide();
} }
} }
...@@ -1170,6 +1181,7 @@ const normalizeNewlines = function(str) { ...@@ -1170,6 +1181,7 @@ const normalizeNewlines = function(str) {
*/ */
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) { Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
const discussionClass = isDiscussionNote ? 'discussion' : ''; const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $( const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
...@@ -1190,7 +1202,7 @@ const normalizeNewlines = function(str) { ...@@ -1190,7 +1202,7 @@ const normalizeNewlines = function(str) {
</div> </div>
<div class="note-body"> <div class="note-body">
<div class="note-text"> <div class="note-text">
<p>${formContent}</p> <p>${escapedFormContent}</p>
</div> </div>
</div> </div>
</div> </div>
...@@ -1320,7 +1332,7 @@ const normalizeNewlines = function(str) { ...@@ -1320,7 +1332,7 @@ const normalizeNewlines = function(str) {
// Show form again on UI on failure // Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) { if (isDiscussionForm && $notesContainer.length) {
const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
$.proxy(this.replyToDiscussionNote, replyButton[0], { target: replyButton[0] }).call(); this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form'); $form = $notesContainer.parent().find('form');
} }
......
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
<div class="mr-widget-heading"> <div class="mr-widget-heading">
<div class="ci-widget"> <div class="ci-widget">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed js-ci-error"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
<span class="js-icon-link icon-link"> <span class="js-icon-link icon-link">
<span <span
v-html="svg" v-html="svg"
......
...@@ -3,30 +3,6 @@ ...@@ -3,30 +3,6 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-entry {
padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
.timeline-entry-inner {
position: relative;
}
&:target {
background: $line-target-blue;
}
.avatar {
margin-right: 15px;
}
.controls {
padding-top: 10px;
float: right;
}
}
.note-text { .note-text {
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0;
...@@ -46,20 +22,45 @@ ...@@ -46,20 +22,45 @@
} }
} }
.timeline-entry {
padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
border-bottom: 1px solid $border-white-light;
.timeline-entry-inner {
position: relative;
}
&:target,
&.target {
background: $line-target-blue;
}
.avatar {
margin-right: 15px;
}
.controls {
padding-top: 10px;
float: right;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.timeline { .timeline {
&::before { &::before {
background: none; background: none;
} }
}
.timeline-entry .timeline-entry-inner { .timeline-entry .timeline-entry-inner {
.timeline-icon { .timeline-icon {
display: none; display: none;
} }
.timeline-content { .timeline-content {
margin-left: 0; margin-left: 0;
}
} }
} }
} }
......
...@@ -109,6 +109,10 @@ ...@@ -109,6 +109,10 @@
height: 22px; height: 22px;
margin-right: 8px; margin-right: 8px;
} }
.ci-error {
margin-right: $btn-side-margin;
}
} }
.mr-widget-body, .mr-widget-body,
......
...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch, :rendered_title, :create_merge_request] :related_branches, :can_create_branch, :realtime_changes, :create_merge_request]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue!, only: [:show, :rendered_title] before_action :authorize_read_issue!, only: [:show, :realtime_changes]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def rendered_title def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000) Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { render json: {
......
...@@ -52,5 +52,15 @@ module EE ...@@ -52,5 +52,15 @@ module EE
def admin_or_auditor? def admin_or_auditor?
admin? || auditor? admin? || auditor?
end end
def remember_me!
return if ::Gitlab::Geo.secondary?
super
end
def forget_me!
return if ::Gitlab::Geo.secondary?
super
end
end end
end end
...@@ -312,7 +312,7 @@ class Issue < ActiveRecord::Base ...@@ -312,7 +312,7 @@ class Issue < ActiveRecord::Base
end end
def expire_etag_cache def expire_etag_cache
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path( key = Gitlab::Routing.url_helpers.realtime_changes_namespace_project_issue_path(
project.namespace, project.namespace,
project, project,
self self
......
module Geo module Geo
class RepositoryBackfillService class RepositorySyncService
attr_reader :project_id attr_reader :project_id
LEASE_TIMEOUT = 8.hours.freeze LEASE_TIMEOUT = 8.hours.freeze
LEASE_KEY_PREFIX = 'repository_backfill_service'.freeze LEASE_KEY_PREFIX = 'repository_sync_service'.freeze
def initialize(project_id) def initialize(project_id)
@project_id = project_id @project_id = project_id
...@@ -81,7 +81,7 @@ module Geo ...@@ -81,7 +81,7 @@ module Geo
end end
def update_registry(started_at, finished_at) def update_registry(started_at, finished_at)
log('Updating registry information') log('Updating repository sync information')
registry = Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id) registry = Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id)
registry.last_repository_synced_at = started_at registry.last_repository_synced_at = started_at
registry.last_repository_successful_sync_at = finished_at if finished_at registry.last_repository_successful_sync_at = finished_at if finished_at
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- if project - if project
:javascript :javascript
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = { gl.GfmAutoComplete.dataSources = {
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
...@@ -11,5 +12,3 @@ ...@@ -11,5 +12,3 @@
milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}", milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
}; };
gl.GfmAutoComplete.setup();
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
%html{ lang: I18n.locale, class: "#{page_class}" } %html{ lang: I18n.locale, class: "#{page_class}" }
= render "layouts/head" = render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= render "layouts/init_auto_complete" if @gfm_form
= render "layouts/header/default", title: header_title = render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav = render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body = yield :scripts_body
= render "layouts/init_auto_complete" if @gfm_form
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
- page_title "Boards" - page_title "Boards"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue') = webpack_bundle_tag 'common_vue'
= page_specific_javascript_bundle_tag('filtered_search') = webpack_bundle_tag 'filtered_search'
= page_specific_javascript_bundle_tag('boards') = webpack_bundle_tag 'boards'
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
......
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,9 @@
= render "projects/issues/head" = render "projects/issues/head"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search') = webpack_bundle_tag 'common_vue'
= page_specific_javascript_bundle_tag('issues') = webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'issues'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
.issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), .issue-title-data.hidden{ "data" => { "endpoint" => realtime_changes_namespace_project_issue_path(@project.namespace, @project, @issue),
"can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '', "can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '',
} } } }
.issue-title-entrypoint .issue-title-entrypoint
......
...@@ -27,7 +27,8 @@ ...@@ -27,7 +27,8 @@
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('vue_merge_request_widget') = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container .content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
= render 'projects/last_push' = render 'projects/last_push'
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search') = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
- if @project.merge_requests.exists? - if @project.merge_requests.exists?
%div{ class: container_class } %div{ class: container_class }
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
dropdown_class: "filtered-search-history-dropdown", dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content", content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do title: "Recent searches" }) do
.js-filtered-search-history-dropdown .js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
.filtered-search-box-input-container .filtered-search-box-input-container
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
......
class GeoBackfillWorker class GeoRepositorySyncWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include CronjobQueue
...@@ -15,20 +15,20 @@ class GeoBackfillWorker ...@@ -15,20 +15,20 @@ class GeoBackfillWorker
project_ids_updated_recently = find_synced_project_ids_updated_recently project_ids_updated_recently = find_synced_project_ids_updated_recently
project_ids = interleave(project_ids_not_synced, project_ids_updated_recently) project_ids = interleave(project_ids_not_synced, project_ids_updated_recently)
logger.info "Started Geo backfilling for #{project_ids.length} project(s)" logger.info "Started Geo repository syncing for #{project_ids.length} project(s)"
project_ids.each do |project_id| project_ids.each do |project_id|
begin begin
break if over_time?(start_time) break if over_time?(start_time)
break unless Gitlab::Geo.current_node_enabled? break unless Gitlab::Geo.current_node_enabled?
# We try to obtain a lease here for the entire backfilling process # We try to obtain a lease here for the entire sync process because we
# because backfill the repositories continuously at a controlled rate # want to sync the repositories continuously at a controlled rate
# instead of hammering the primary node. Initially, we are backfilling # instead of hammering the primary node. Initially, we are syncing
# one repo at a time. If we don't obtain the lease here, every 5 # one repo at a time. If we don't obtain the lease here, every 5
# minutes all of 100 projects will be synced. # minutes all of 100 projects will be synced.
try_obtain_lease do |lease| try_obtain_lease do |lease|
Geo::RepositoryBackfillService.new(project_id).execute Geo::RepositorySyncService.new(project_id).execute
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
logger.error("Couldn't find project with ID=#{project_id}, skipping syncing") logger.error("Couldn't find project with ID=#{project_id}, skipping syncing")
...@@ -36,7 +36,7 @@ class GeoBackfillWorker ...@@ -36,7 +36,7 @@ class GeoBackfillWorker
end end
end end
logger.info "Finished Geo backfilling for #{project_ids.length} project(s)" logger.info "Finished Geo repository syncing for #{project_ids.length} project(s)"
end end
private private
...@@ -86,10 +86,10 @@ class GeoBackfillWorker ...@@ -86,10 +86,10 @@ class GeoBackfillWorker
end end
def lease_key def lease_key
Geo::RepositoryBackfillService::LEASE_KEY_PREFIX Geo::RepositorySyncService::LEASE_KEY_PREFIX
end end
def lease_timeout def lease_timeout
Geo::RepositoryBackfillService::LEASE_TIMEOUT Geo::RepositorySyncService::LEASE_TIMEOUT
end end
end end
---
title: Geo - Fix signing out from secondary node when "Remember me" option is checked
merge_request: 1903
author:
---
title: Scope issue/merge request recent searches to project
merge_request:
author:
...@@ -216,14 +216,14 @@ production: &base ...@@ -216,14 +216,14 @@ production: &base
geo_bulk_notify_worker: geo_bulk_notify_worker:
cron: "*/10 * * * * *" cron: "*/10 * * * * *"
# GitLab Geo backfill worker # GitLab Geo repository sync worker
# NOTE: This will only take effect if Geo is enabled # NOTE: This will only take effect if Geo is enabled
geo_backfill_worker: geo_repository_sync_worker:
cron: "*/5 * * * *" cron: "*/5 * * * *"
# GitLab Geo file download worker # GitLab Geo file download dispatch worker
# NOTE: This will only take effect if Geo is enabled # NOTE: This will only take effect if Geo is enabled
geo_download_dispatch_worker: geo_file_download_dispatch_worker:
cron: "*/10 * * * *" cron: "*/10 * * * *"
registry: registry:
......
...@@ -396,12 +396,12 @@ Settings.cron_jobs['ldap_group_sync_worker']['job_class'] = 'LdapGroupSyncWorker ...@@ -396,12 +396,12 @@ Settings.cron_jobs['ldap_group_sync_worker']['job_class'] = 'LdapGroupSyncWorker
Settings.cron_jobs['geo_bulk_notify_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_bulk_notify_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_bulk_notify_worker']['cron'] ||= '*/10 * * * * *' Settings.cron_jobs['geo_bulk_notify_worker']['cron'] ||= '*/10 * * * * *'
Settings.cron_jobs['geo_bulk_notify_worker']['job_class'] ||= 'GeoBulkNotifyWorker' Settings.cron_jobs['geo_bulk_notify_worker']['job_class'] ||= 'GeoBulkNotifyWorker'
Settings.cron_jobs['geo_backfill_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_repository_sync_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_backfill_worker']['cron'] ||= '*/5 * * * *' Settings.cron_jobs['geo_repository_sync_worker']['cron'] ||= '*/5 * * * *'
Settings.cron_jobs['geo_backfill_worker']['job_class'] ||= 'GeoBackfillWorker' Settings.cron_jobs['geo_repository_sync_worker']['job_class'] ||= 'GeoRepositorySyncWorker'
Settings.cron_jobs['geo_download_dispatch_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_download_dispatch_worker']['cron'] ||= '5 * * * *' Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '5 * * * *'
Settings.cron_jobs['geo_download_dispatch_worker']['job_class'] ||= 'GeoFileDownloadDispatchWorker' Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'GeoFileDownloadDispatchWorker'
Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.send(:cron_random_weekly_time) Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.send(:cron_random_weekly_time)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker' Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
......
...@@ -279,7 +279,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -279,7 +279,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests get :referenced_merge_requests
get :related_branches get :related_branches
get :can_create_branch get :can_create_branch
get :rendered_title get :realtime_changes
post :create_merge_request post :create_merge_request
end end
collection do collection do
......
...@@ -143,16 +143,24 @@ var config = { ...@@ -143,16 +143,24 @@ var config = {
'diff_notes', 'diff_notes',
'environments', 'environments',
'environments_folder', 'environments_folder',
'sidebar', 'filtered_search',
'issue_show', 'issue_show',
'merge_conflicts', 'merge_conflicts',
'notebook_viewer', 'notebook_viewer',
'pdf_viewer', 'pdf_viewer',
'pipelines', 'pipelines',
<<<<<<< HEAD
'mr_widget_ee', 'mr_widget_ee',
'issue_show', 'issue_show',
'balsamiq_viewer', 'balsamiq_viewer',
'pipelines_graph', 'pipelines_graph',
=======
'pipelines_graph',
'schedule_form',
'schedules_index',
'sidebar',
'vue_merge_request_widget',
>>>>>>> origin/master
], ],
minChunks: function(module, count) { minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource); return module.resource && (/vue_shared/).test(module.resource);
......
...@@ -76,14 +76,21 @@ Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, b ...@@ -76,14 +76,21 @@ Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, b
We try to treat documentation as code, thus have implemented some testing. We try to treat documentation as code, thus have implemented some testing.
Currently, the following tests are in place: Currently, the following tests are in place:
1. `docs:check:links`: Check that all internal (relative) links work correctly 1. `docs lint`: Check that all internal (relative) links work correctly and
1. `docs:check:apilint`: Check that the API docs follow some conventions that all cURL examples in API docs use the full switches.
If your contribution contains **only** documentation changes, you can speed up If your contribution contains **only** documentation changes, you can speed up
the CI process by prepending to the name of your branch: `docs/`. For example, the CI process by following some branch naming conventions. You have three
a valid name would be `docs/update-api-issues` and it will run only the docs choices:
tests. If the name is `docs-update-api-issues`, the whole test suite will run
(including docs). | Branch name | Valid example |
| ----------- | ------------- |
| Starting with `docs/` | `docs/update-api-issues` |
| Starting with `docs-` | `docs-update-api-issues` |
| Ending in `-docs` | `123-update-api-issues-docs` |
If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs).
--- ---
......
...@@ -37,7 +37,7 @@ merge request, they automatically get excluded from the approvers list. ...@@ -37,7 +37,7 @@ merge request, they automatically get excluded from the approvers list.
### Approvers ### Approvers
At the approvers area you can define the default set of users that need to At the approvers area you can select the default set of users that need to
approve a merge request. approve a merge request.
Depending on the number of required approvals and the number of approvers set, Depending on the number of required approvals and the number of approvers set,
...@@ -56,7 +56,14 @@ creating or editing a merge request. ...@@ -56,7 +56,14 @@ creating or editing a merge request.
When someone is marked as a required approver for a merge request, an email is When someone is marked as a required approver for a merge request, an email is
sent to them and a todo is added to their list of todos. sent to them and a todo is added to their list of todos.
### Approver groups ### Selecting individual approvers
GitLab restricts the users that can be selected to be individual approvers. Only these can be selected and appear in the search box:
- Members of the current project
- Members of the parent group of the current project
- Members of a group that have access to the current project [via a share](../../../workflow/share_projects_with_other_groups.md)
### Selecting group approvers
> [Introduced][ee-743] in GitLab Enterprise Edition 8.13. > [Introduced][ee-743] in GitLab Enterprise Edition 8.13.
......
...@@ -7,8 +7,8 @@ module Gitlab ...@@ -7,8 +7,8 @@ module Gitlab
# - Don't contain a reserved word (expect for the words used in the # - Don't contain a reserved word (expect for the words used in the
# regex itself) # regex itself)
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route # - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/rendered_title` for the `issue_title` route # - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues rendered_title USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze commit pipelines merge_requests new].freeze
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
'issue_notes' 'issue_notes'
), ),
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/rendered_title\z), %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/realtime_changes\z),
'issue_title' 'issue_title'
), ),
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
).freeze ).freeze
PRIMARY_JOBS = %i(bulk_notify_job).freeze PRIMARY_JOBS = %i(bulk_notify_job).freeze
SECONDARY_JOBS = %i(backfill_job file_download_job).freeze SECONDARY_JOBS = %i(repository_sync_job file_download_job).freeze
def self.current_node def self.current_node
self.cache_value(:geo_node_current) do self.cache_value(:geo_node_current) do
...@@ -37,7 +37,7 @@ module Gitlab ...@@ -37,7 +37,7 @@ module Gitlab
def self.current_node_enabled? def self.current_node_enabled?
# No caching of the enabled! If we cache it and an admin disables # No caching of the enabled! If we cache it and an admin disables
# this node, an active GeoBackfillWorker would keep going for up # this node, an active GeoRepositorySyncWorker would keep going for up
# to max run time after the node was disabled. # to max run time after the node was disabled.
Gitlab::Geo.current_node.reload.enabled? Gitlab::Geo.current_node.reload.enabled?
end end
...@@ -74,12 +74,12 @@ module Gitlab ...@@ -74,12 +74,12 @@ module Gitlab
Sidekiq::Cron::Job.find('geo_bulk_notify_worker') Sidekiq::Cron::Job.find('geo_bulk_notify_worker')
end end
def self.backfill_job def self.repository_sync_job
Sidekiq::Cron::Job.find('geo_backfill_worker') Sidekiq::Cron::Job.find('geo_repository_sync_worker')
end end
def self.file_download_job def self.file_download_job
Sidekiq::Cron::Job.find('geo_download_dispatch_worker') Sidekiq::Cron::Job.find('geo_file_download_dispatch_worker')
end end
def self.configure_primary_jobs! def self.configure_primary_jobs!
......
...@@ -3,17 +3,17 @@ require 'spec_helper' ...@@ -3,17 +3,17 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers include FilteredSearchHelpers
let!(:group) { create(:group) } let(:project_1) { create(:empty_project, :public) }
let!(:project) { create(:project, group: group) } let(:project_2) { create(:empty_project, :public) }
let!(:user) { create(:user) } let(:project_1_local_storage_key) { "#{project_1.full_path}-issue-recent-searches" }
before do before do
Capybara.ignore_hidden_elements = false Capybara.ignore_hidden_elements = false
project.add_master(user) create(:issue, project: project_1)
group.add_developer(user) create(:issue, project: project_2)
create(:issue, project: project)
login_as(user)
# Visit any fast-loading page so we can clear local storage without a DOM exception
visit '/404'
remove_recent_searches remove_recent_searches
end end
...@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do ...@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'searching adds to recent searches' do it 'searching adds to recent searches' do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
input_filtered_search('foo', submit: true) input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true) input_filtered_search('bar', submit: true)
...@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'visiting URL with search params adds to recent searches' do it 'visiting URL with search params adds to recent searches' do
visit namespace_project_issues_path(project.namespace, project, label_name: 'foo', search: 'bar') visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'foo', search: 'bar')
visit namespace_project_issues_path(project.namespace, project, label_name: 'qux', search: 'garply') visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'qux', search: 'garply')
items = all('.filtered-search-history-dropdown-item', visible: false) items = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do ...@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'saved recent searches are restored last on the list' do it 'saved recent searches are restored last on the list' do
set_recent_searches('["saved1", "saved2"]') set_recent_searches(project_1_local_storage_key, '["saved1", "saved2"]')
visit namespace_project_issues_path(project.namespace, project, search: 'foo') visit namespace_project_issues_path(project_1.namespace, project_1, search: 'foo')
items = all('.filtered-search-history-dropdown-item', visible: false) items = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do ...@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do
expect(items[2].text).to eq('saved2') expect(items[2].text).to eq('saved2')
end end
it 'searches are scoped to projects' do
visit namespace_project_issues_path(project_1.namespace, project_1)
input_filtered_search('foo', submit: true)
input_filtered_search('bar', submit: true)
visit namespace_project_issues_path(project_2.namespace, project_2)
input_filtered_search('more', submit: true)
input_filtered_search('things', submit: true)
items = all('.filtered-search-history-dropdown-item', visible: false)
expect(items.count).to eq(2)
expect(items[0].text).to eq('things')
expect(items[1].text).to eq('more')
end
it 'clicking item fills search input' do it 'clicking item fills search input' do
set_recent_searches('["foo", "bar"]') set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click') all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
wait_for_filtered_search('foo') wait_for_filtered_search('foo')
...@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'clear recent searches button, clears recent searches' do it 'clear recent searches button, clears recent searches' do
set_recent_searches('["foo"]') set_recent_searches(project_1_local_storage_key, '["foo"]')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
items_before = all('.filtered-search-history-dropdown-item', visible: false) items_before = all('.filtered-search-history-dropdown-item', visible: false)
...@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do ...@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do
end end
it 'shows flash error when failed to parse saved history' do it 'shows flash error when failed to parse saved history' do
set_recent_searches('fail') set_recent_searches(project_1_local_storage_key, 'fail')
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project_1.namespace, project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches') expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
end end
......
...@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do ...@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end end
it 'does not load on project#show' do it 'does not load on project#show' do
expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({}) expect(evaluate_script('gl.GfmAutoComplete')).to eq(nil)
end end
it 'loads on new issue page' do it 'loads on new issue page' do
......
/* eslint no-param-reassign: "off" */ /* eslint no-param-reassign: "off" */
require('~/gfm_auto_complete'); import GfmAutoComplete from '~/gfm_auto_complete';
require('vendor/jquery.caret'); require('vendor/jquery.caret');
require('vendor/jquery.atwho'); require('vendor/jquery.atwho');
const global = window.gl || (window.gl = {});
const GfmAutoComplete = global.GfmAutoComplete;
describe('GfmAutoComplete', function () { describe('GfmAutoComplete', function () {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
fetchData: () => {},
});
describe('DefaultOptions.sorter', function () { describe('DefaultOptions.sorter', function () {
describe('assets loading', function () { describe('assets loading', function () {
beforeEach(function () { beforeEach(function () {
...@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () { ...@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () {
this.atwhoInstance = { setting: {} }; this.atwhoInstance = { setting: {} };
this.items = []; this.items = [];
this.sorterValue = GfmAutoComplete.DefaultOptions.sorter this.sorterValue = gfmAutoCompleteCallbacks.sorter
.call(this.atwhoInstance, '', this.items); .call(this.atwhoInstance, '', this.items);
}); });
...@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () { ...@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if alwaysHighlightFirst is set', function () { it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
const atwhoInstance = { setting: { alwaysHighlightFirst: true } }; const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance); gfmAutoCompleteCallbacks.sorter.call(atwhoInstance);
expect(atwhoInstance.setting.highlightFirst).toBe(true); expect(atwhoInstance.setting.highlightFirst).toBe(true);
}); });
...@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () { ...@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if a query is present', function () { it('should enable highlightFirst if a query is present', function () {
const atwhoInstance = { setting: {} }; const atwhoInstance = { setting: {} };
GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query'); gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query');
expect(atwhoInstance.setting.highlightFirst).toBe(true); expect(atwhoInstance.setting.highlightFirst).toBe(true);
}); });
...@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () { ...@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () {
const items = []; const items = [];
const searchKey = 'searchKey'; const searchKey = 'searchKey';
GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey); gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey);
expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey); expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
}); });
...@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () { ...@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () {
describe('DefaultOptions.matcher', function () { describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => ( const defaultMatcher = (context, flag, subtext) => (
GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
); );
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%'];
......
...@@ -35,7 +35,7 @@ describe('Issue Title', () => { ...@@ -35,7 +35,7 @@ describe('Issue Title', () => {
const issueShowComponent = new IssueTitleDescriptionComponent({ const issueShowComponent = new IssueTitleDescriptionComponent({
propsData: { propsData: {
canUpdateIssue: '.css-stuff', canUpdateIssue: '.css-stuff',
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title', endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
}, },
}).$mount(); }).$mount();
......
...@@ -377,7 +377,7 @@ import '~/notes'; ...@@ -377,7 +377,7 @@ import '~/notes';
}); });
it('should return true when comment begins with a slash command', () => { it('should return true when comment begins with a slash command', () => {
const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this'; const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); const hasSlashCommands = this.notes.hasSlashCommands(sampleComment);
expect(hasSlashCommands).toBeTruthy(); expect(hasSlashCommands).toBeTruthy();
...@@ -401,10 +401,18 @@ import '~/notes'; ...@@ -401,10 +401,18 @@ import '~/notes';
describe('stripSlashCommands', () => { describe('stripSlashCommands', () => {
it('should strip slash commands from the comment which begins with a slash command', () => { it('should strip slash commands from the comment which begins with a slash command', () => {
this.notes = new Notes(); this.notes = new Notes();
const sampleComment = '/wip \n/milestone %1.0 \n/merge \n/unassign Merging this'; const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment); const stripedComment = this.notes.stripSlashCommands(sampleComment);
expect(stripedComment).not.toBe(sampleComment); expect(stripedComment).toBe('');
});
it('should strip slash commands from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment);
expect(stripedComment).toBe('Merging this');
}); });
it('should NOT strip string that has slashes within', () => { it('should NOT strip string that has slashes within', () => {
...@@ -424,6 +432,22 @@ import '~/notes'; ...@@ -424,6 +432,22 @@ import '~/notes';
beforeEach(() => { beforeEach(() => {
this.notes = new Notes('', []); this.notes = new Notes('', []);
spyOn(_, 'escape').and.callFake((comment) => {
const escapedString = comment.replace(/["&'<>]/g, (a) => {
const escapedToken = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;'
}[a];
return escapedToken;
});
return escapedString;
});
}); });
it('should return constructed placeholder element for regular note based on form contents', () => { it('should return constructed placeholder element for regular note based on form contents', () => {
...@@ -444,7 +468,21 @@ import '~/notes'; ...@@ -444,7 +468,21 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy(); expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname); expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`); expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
expect($tempNote.find('.note-body .note-text').text().trim()).toEqual(sampleComment); expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment);
});
it('should escape HTML characters from note based on form contents', () => {
const commentWithHtml = '<script>alert("Boom!");</script>';
const $tempNote = this.notes.createPlaceholderNote({
formContent: commentWithHtml,
uniqueId,
isDiscussionNote: false,
currentUsername,
currentUserFullname
});
expect(_.escape).toHaveBeenCalledWith(commentWithHtml);
expect($tempNote.find('.note-body .note-text p').html()).toEqual('&lt;script&gt;alert("Boom!");&lt;/script&gt;');
}); });
it('should return constructed placeholder element for discussion note based on form contents', () => { it('should return constructed placeholder element for discussion note based on form contents', () => {
......
...@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do ...@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it 'matches issue title endpoint' do it 'matches issue title endpoint' do
env = build_env( env = build_env(
'/my-group/my-project/issues/123/rendered_title' '/my-group/my-project/issues/123/realtime_changes'
) )
result = described_class.match(env) result = described_class.match(env)
......
...@@ -122,11 +122,8 @@ describe Gitlab::Geo, lib: true do ...@@ -122,11 +122,8 @@ describe Gitlab::Geo, lib: true do
end end
before(:all) do before(:all) do
jobs = %w(geo_bulk_notify_worker geo_backfill_worker) jobs = %w(geo_bulk_notify_worker geo_repository_sync_worker geo_file_download_dispatch_worker)
jobs.each { |job| init_cron_job(job, job.camelize) } jobs.each { |job| init_cron_job(job, job.camelize) }
# TODO: Make this name consistent
init_cron_job('geo_download_dispatch_worker', 'GeoFileDownloadDispatchWorker')
end end
it 'activates cron jobs for primary' do it 'activates cron jobs for primary' do
...@@ -134,7 +131,7 @@ describe Gitlab::Geo, lib: true do ...@@ -134,7 +131,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).to be_enabled expect(described_class.bulk_notify_job).to be_enabled
expect(described_class.backfill_job).not_to be_enabled expect(described_class.repository_sync_job).not_to be_enabled
expect(described_class.file_download_job).not_to be_enabled expect(described_class.file_download_job).not_to be_enabled
end end
...@@ -143,7 +140,7 @@ describe Gitlab::Geo, lib: true do ...@@ -143,7 +140,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled expect(described_class.bulk_notify_job).not_to be_enabled
expect(described_class.backfill_job).to be_enabled expect(described_class.repository_sync_job).to be_enabled
expect(described_class.file_download_job).to be_enabled expect(described_class.file_download_job).to be_enabled
end end
...@@ -151,7 +148,7 @@ describe Gitlab::Geo, lib: true do ...@@ -151,7 +148,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs! described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled expect(described_class.bulk_notify_job).not_to be_enabled
expect(described_class.backfill_job).not_to be_enabled expect(described_class.repository_sync_job).not_to be_enabled
expect(described_class.file_download_job).not_to be_enabled expect(described_class.file_download_job).not_to be_enabled
end end
end end
......
...@@ -1967,4 +1967,36 @@ describe User, models: true do ...@@ -1967,4 +1967,36 @@ describe User, models: true do
expect(user.preferred_language).to eq('en') expect(user.preferred_language).to eq('en')
end end
end end
describe '#forget_me!' do
subject { create(:user, remember_created_at: Time.now) }
it 'clears remember_created_at' do
subject.forget_me!
expect(subject.reload.remember_created_at).to be_nil
end
it 'does not clear remember_created_at when in a Geo secondary node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
expect { subject.forget_me! }.not_to change(subject, :remember_created_at)
end
end
describe '#remember_me!' do
subject { create(:user, remember_created_at: nil) }
it 'updates remember_created_at' do
subject.remember_me!
expect(subject.reload.remember_created_at).not_to be_nil
end
it 'does not update remember_created_at when in a Geo secondary node' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
expect { subject.remember_me! }.not_to change(subject, :remember_created_at)
end
end
end end
...@@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do ...@@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do
) )
end end
it { expect(pipeline).to be_kind_of(Ci::Pipeline) } it 'creates a pipeline' do
it { expect(pipeline).to be_valid } expect(pipeline).to be_kind_of(Ci::Pipeline)
it { expect(pipeline).to eq(project.pipelines.last) } expect(pipeline).to be_valid
it { expect(pipeline).to have_attributes(user: user) } expect(pipeline).to eq(project.pipelines.last)
it { expect(pipeline).to have_attributes(status: 'pending') } expect(pipeline).to have_attributes(user: user)
it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } expect(pipeline).to have_attributes(status: 'pending')
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
end
context '#update_merge_requests_head_pipeline' do context '#update_merge_requests_head_pipeline' do
it 'updates head pipeline of each merge request' do it 'updates head pipeline of each merge request' do
......
require 'spec_helper' require 'spec_helper'
describe Geo::RepositoryBackfillService, services: true do describe Geo::RepositorySyncService, services: true do
let!(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') } let!(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') }
subject { described_class.new(project.id) } subject { described_class.new(project.id) }
...@@ -106,7 +106,7 @@ describe Geo::RepositoryBackfillService, services: true do ...@@ -106,7 +106,7 @@ describe Geo::RepositoryBackfillService, services: true do
end end
end end
context 'when repository was backfilled successfully' do context 'when repository was synced successfully' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:last_repository_synced_at) { 5.days.ago } let(:last_repository_synced_at) { 5.days.ago }
...@@ -159,7 +159,7 @@ describe Geo::RepositoryBackfillService, services: true do ...@@ -159,7 +159,7 @@ describe Geo::RepositoryBackfillService, services: true do
end end
end end
context 'when last attempt to backfill the repository failed' do context 'when last attempt to sync the repository failed' do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:registry) do let!(:registry) do
......
...@@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do ...@@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do
end end
end end
it { expect(issue).to be_valid } it 'closes the issue' do
it { expect(issue).to be_closed } expect(issue).to be_valid
expect(issue).to be_closed
end
it 'sends email to user2 about assign of new issue' do it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last email = ActionMailer::Base.deliveries.last
...@@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do ...@@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do
described_class.new(project, user).close_issue(issue) described_class.new(project, user).close_issue(issue)
end end
it { expect(issue).to be_valid } it 'closes the issue' do
it { expect(issue).to be_opened } expect(issue).to be_valid
it { expect(todo.reload).to be_pending } expect(issue).to be_opened
expect(todo.reload).to be_pending
end
end end
end end
end end
...@@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do ...@@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do
@merge_request = service.execute @merge_request = service.execute
end end
it { expect(@merge_request).to be_valid } it 'creates an MR' do
it { expect(@merge_request.title).to eq('Awesome merge_request') } expect(@merge_request).to be_valid
it { expect(@merge_request.assignee).to be_nil } expect(@merge_request.title).to eq('Awesome merge_request')
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } expect(@merge_request.assignee).to be_nil
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
it 'executes hooks with default action' do it 'executes hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request) expect(service).to have_received(:execute_hooks).with(@merge_request)
......
...@@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do ...@@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
it { expect(@merge_request).to be_valid } it 'mathces base expectations' do
it { expect(@merge_request.title).to eq('New title') } expect(@merge_request).to be_valid
it { expect(@merge_request.assignee).to eq(user2) } expect(@merge_request.title).to eq('New title')
it { expect(@merge_request).to be_closed } expect(@merge_request.assignee).to eq(user2)
it { expect(@merge_request.labels.count).to eq(1) } expect(@merge_request).to be_closed
it { expect(@merge_request.labels.first.title).to eq(label.name) } expect(@merge_request.labels.count).to eq(1)
it { expect(@merge_request.target_branch).to eq('target') } expect(@merge_request.labels.first.title).to eq(label.name)
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
it 'executes hooks with update action' do it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks). expect(service).to have_received(:execute_hooks).
...@@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do ...@@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
it { expect(@merge_request).to be_valid } it 'merges the MR' do
it { expect(@merge_request.state).to eq('merged') } expect(@merge_request).to be_valid
it { expect(@merge_request.merge_error).to be_nil } expect(@merge_request.state).to eq('merged')
expect(@merge_request.merge_error).to be_nil
end
end end
context 'with finished pipeline' do context 'with finished pipeline' do
...@@ -167,8 +171,10 @@ describe MergeRequests::UpdateService, services: true do ...@@ -167,8 +171,10 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
it { expect(@merge_request).to be_valid } it 'merges the MR' do
it { expect(@merge_request.state).to eq('merged') } expect(@merge_request).to be_valid
expect(@merge_request.state).to eq('merged')
end
end end
context 'with active pipeline' do context 'with active pipeline' do
...@@ -202,8 +208,10 @@ describe MergeRequests::UpdateService, services: true do ...@@ -202,8 +208,10 @@ describe MergeRequests::UpdateService, services: true do
end end
end end
it { expect(@merge_request.state).to eq('opened') } it 'does not merge the MR' do
it { expect(@merge_request.merge_error).not_to be_nil } expect(@merge_request.state).to eq('opened')
expect(@merge_request.merge_error).not_to be_nil
end
end end
context 'MR can not be merged when note sha != MR sha' do context 'MR can not be merged when note sha != MR sha' do
......
...@@ -73,11 +73,11 @@ module FilteredSearchHelpers ...@@ -73,11 +73,11 @@ module FilteredSearchHelpers
end end
def remove_recent_searches def remove_recent_searches
execute_script('window.localStorage.removeItem(\'issue-recent-searches\');') execute_script('window.localStorage.clear();')
end end
def set_recent_searches(input) def set_recent_searches(key, input)
execute_script("window.localStorage.setItem('issue-recent-searches', '#{input}');") execute_script("window.localStorage.setItem('#{key}', '#{input}');")
end end
def wait_for_filtered_search(text) def wait_for_filtered_search(text)
......
require 'spec_helper' require 'spec_helper'
describe Geo::GeoBackfillWorker, services: true do describe Geo::GeoRepositorySyncWorker, services: true do
let!(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') } let!(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') }
let!(:secondary) { create(:geo_node, :current) } let!(:secondary) { create(:geo_node, :current) }
let!(:project_1) { create(:empty_project) } let!(:project_1) { create(:empty_project) }
...@@ -13,25 +13,25 @@ describe Geo::GeoBackfillWorker, services: true do ...@@ -13,25 +13,25 @@ describe Geo::GeoBackfillWorker, services: true do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { true } allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { true }
end end
it 'performs Geo::RepositoryBackfillService for each project' do it 'performs Geo::RepositorySyncService for each project' do
expect(Geo::RepositoryBackfillService).to receive(:new).twice.and_return(spy) expect(Geo::RepositorySyncService).to receive(:new).twice.and_return(spy)
subject.perform subject.perform
end end
it 'performs Geo::RepositoryBackfillService for projects where last attempt to backfill failed' do it 'performs Geo::RepositorySyncService for projects where last attempt to sync failed' do
Geo::ProjectRegistry.create( Geo::ProjectRegistry.create(
project: project_1, project: project_1,
last_repository_synced_at: DateTime.now, last_repository_synced_at: DateTime.now,
last_repository_successful_sync_at: nil last_repository_successful_sync_at: nil
) )
expect(Geo::RepositoryBackfillService).to receive(:new).twice.and_return(spy) expect(Geo::RepositorySyncService).to receive(:new).twice.and_return(spy)
subject.perform subject.perform
end end
it 'performs Geo::RepositoryBackfillService for backfilled projects updated recently' do it 'performs Geo::RepositorySyncService for synced projects updated recently' do
Geo::ProjectRegistry.create( Geo::ProjectRegistry.create(
project: project_1, project: project_1,
last_repository_synced_at: 2.days.ago, last_repository_synced_at: 2.days.ago,
...@@ -47,39 +47,39 @@ describe Geo::GeoBackfillWorker, services: true do ...@@ -47,39 +47,39 @@ describe Geo::GeoBackfillWorker, services: true do
project_1.update_attribute(:last_repository_updated_at, 2.days.ago) project_1.update_attribute(:last_repository_updated_at, 2.days.ago)
project_2.update_attribute(:last_repository_updated_at, 10.minutes.ago) project_2.update_attribute(:last_repository_updated_at, 10.minutes.ago)
expect(Geo::RepositoryBackfillService).to receive(:new).once.and_return(spy) expect(Geo::RepositorySyncService).to receive(:new).once.and_return(spy)
subject.perform subject.perform
end end
it 'does not perform Geo::RepositoryBackfillService when tracking DB is not available' do it 'does not perform Geo::RepositorySyncService when tracking DB is not available' do
allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false } allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false }
expect(Geo::RepositoryBackfillService).not_to receive(:new) expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform subject.perform
end end
it 'does not perform Geo::RepositoryBackfillService when primary node does not exists' do it 'does not perform Geo::RepositorySyncService when primary node does not exists' do
allow(Gitlab::Geo).to receive(:primary_node) { nil } allow(Gitlab::Geo).to receive(:primary_node) { nil }
expect(Geo::RepositoryBackfillService).not_to receive(:new) expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform subject.perform
end end
it 'does not perform Geo::RepositoryBackfillService when node is disabled' do it 'does not perform Geo::RepositorySyncService when node is disabled' do
allow_any_instance_of(GeoNode).to receive(:enabled?) { false } allow_any_instance_of(GeoNode).to receive(:enabled?) { false }
expect(Geo::RepositoryBackfillService).not_to receive(:new) expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform subject.perform
end end
it 'does not perform Geo::RepositoryBackfillService when can not obtain a lease' do it 'does not perform Geo::RepositorySyncService when can not obtain a lease' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { false } allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { false }
expect(Geo::RepositoryBackfillService).not_to receive(:new) expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform subject.perform
end end
......
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