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:
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
.except-docs: &except-docs
except:
- /^docs\/.*/
- /(^docs[\/-].*|.*-docs$)/
.rspec-knapsack: &rspec-knapsack
stage: test
......@@ -313,7 +313,7 @@ downtime_check:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /^docs\/*/
- /(^docs[\/-].*|.*-docs$)/
.db-migrate-reset: &db-migrate-reset
stage: test
......
......@@ -120,7 +120,7 @@ const DiffNoteAvatars = Vue.extend({
},
methods: {
clickedAvatar(e) {
notes.addDiffNote(e);
notes.onAddDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
......
......@@ -57,6 +57,7 @@ import BlobViewer from './blob/viewer/index';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import GfmAutoComplete from './gfm_auto_complete';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -83,6 +84,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path = page.split(':');
shortcut_handler = null;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
function initBlob() {
new LineHighlighter();
......
......@@ -20,10 +20,14 @@ class FilteredSearchManager {
this.recentSearchesStore = new RecentSearchesStore({
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') {
recentSearchesKey = 'merge-request-recent-searches';
recentSearchesPagePrefix = 'merge-request-recent-searches';
}
const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
// Fetch recent searches from localStorage
......@@ -51,7 +55,7 @@ class FilteredSearchManager {
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
this.recentSearchesService,
document.querySelector('.js-filtered-search-history-dropdown'),
searchHistoryDropdownElement,
);
this.recentSearchesRoot.init();
......
......@@ -3,6 +3,7 @@ import _ from 'underscore';
class RecentSearchesStore {
constructor(initialState = {}) {
this.state = Object.assign({
isLocalStorageAvailable: true,
recentSearches: [],
}, initialState);
}
......
This diff is collapsed.
......@@ -3,6 +3,8 @@
/* global DropzoneInput */
/* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {};
function GLForm(form) {
......@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() {
// 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.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);
autosize(this.textarea);
}
......
......@@ -7,6 +7,8 @@
/* global dateFormat */
/* global Pikaday */
import GfmAutoComplete from './gfm_auto_complete';
(function() {
this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
......@@ -20,7 +22,7 @@
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
gl.GfmAutoComplete.setup();
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new GroupsSelect();
new ZenMode();
......
......@@ -97,7 +97,6 @@ import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
import './gfm_auto_complete';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
......
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
import './breakpoints';
......@@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.ajaxGet({
url: `${urlPathname}.json${location.search}`,
success: (data) => {
$('#diffs').html(data.html);
const $container = $('#diffs');
$container.html(data.html);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
......@@ -278,6 +280,20 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
})
.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');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('./task_list');
......@@ -24,7 +23,7 @@ const normalizeNewlines = function(str) {
(function() {
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_SLASH_COMMANDS = /^\/\w+/gm;
const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm;
Notes.interval = null;
......@@ -33,9 +32,9 @@ const normalizeNewlines = function(str) {
this.updateComment = this.updateComment.bind(this);
this.visibilityChange = this.visibilityChange.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.replyToDiscussionNote = this.replyToDiscussionNote.bind(this);
this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
this.removeNote = this.removeNote.bind(this);
this.cancelEdit = this.cancelEdit.bind(this);
this.updateNote = this.updateNote.bind(this);
......@@ -100,9 +99,9 @@ const normalizeNewlines = function(str) {
// update the file name when an attachment is selected
$(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
// 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
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
$(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote);
// hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
// toggle commit list
......@@ -794,10 +793,14 @@ const normalizeNewlines = function(str) {
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;
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
replyLink
.closest('.discussion-reply-holder')
......@@ -867,35 +870,43 @@ const normalizeNewlines = function(str) {
Sets up the form and shows it.
*/
Notes.prototype.addDiffNote = function(e) {
var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
Notes.prototype.onAddDiffNote = function(e) {
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");
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
const nextRow = row.next();
let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
hasNotes = targetRow.is(".notes_holder");
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>";
isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
notesContentSelector += "." + lineType;
lineTypeSelector = `.${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>";
}
notesContentSelector += " .content";
notesContent = nextRow.find(notesContentSelector);
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
if (hasNotes && !isDiffCommentAvatar) {
nextRow.show();
notesContent = nextRow.find(notesContentSelector);
if (hasNotes && showReplyInput) {
targetRow.show();
notesContent = targetRow.find(notesContentSelector);
if (notesContent.length) {
notesContent.show();
replyButton = notesContent.find(".js-discussion-reply-button:visible");
if (replyButton.length) {
e.target = replyButton[0];
$.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
this.replyToDiscussionNote(replyButton[0]);
} else {
// In parallel view, the form may not be present in one of the panes
noteForm = notesContent.find(".js-discussion-note-form");
......@@ -904,18 +915,18 @@ const normalizeNewlines = function(str) {
}
}
}
} else if (!isDiffCommentAvatar) {
} else if (showReplyInput) {
// add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
targetRow = row.next();
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
nextRow.show();
targetRow.show();
notesContent.toggle(!notesContent.is(':visible'));
if (!nextRow.find('.content:not(:empty)').is(':visible')) {
nextRow.hide();
if (!targetRow.find('.content:not(:empty)').is(':visible')) {
targetRow.hide();
}
}
......@@ -1170,6 +1181,7 @@ const normalizeNewlines = function(str) {
*/
Notes.prototype.createPlaceholderNote = function({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const escapedFormContent = _.escape(formContent);
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
......@@ -1190,7 +1202,7 @@ const normalizeNewlines = function(str) {
</div>
<div class="note-body">
<div class="note-text">
<p>${formContent}</p>
<p>${escapedFormContent}</p>
</div>
</div>
</div>
......@@ -1320,7 +1332,7 @@ const normalizeNewlines = function(str) {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
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');
}
......
......@@ -34,7 +34,7 @@ export default {
<div class="mr-widget-heading">
<div class="ci-widget">
<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
v-html="svg"
......
......@@ -3,7 +3,26 @@
margin: 0;
padding: 0;
.timeline-entry {
.note-text {
p:last-child {
margin-bottom: 0;
}
}
.system-note {
.note-text {
color: $gl-text-color !important;
}
}
.diff-file {
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
}
}
.timeline-entry {
padding: $gl-padding $gl-btn-padding 0;
border-color: $white-normal;
color: $gl-text-color;
......@@ -13,7 +32,8 @@
position: relative;
}
&:target {
&:target,
&.target {
background: $line-target-blue;
}
......@@ -25,25 +45,6 @@
padding-top: 10px;
float: right;
}
}
.note-text {
p:last-child {
margin-bottom: 0;
}
}
.system-note {
.note-text {
color: $gl-text-color !important;
}
}
.diff-file {
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
}
}
@media (max-width: $screen-xs-max) {
......@@ -51,6 +52,7 @@
&::before {
background: none;
}
}
.timeline-entry .timeline-entry-inner {
.timeline-icon {
......@@ -61,7 +63,6 @@
margin-left: 0;
}
}
}
}
.discussion .timeline-entry {
......
......@@ -109,6 +109,10 @@
height: 22px;
margin-right: 8px;
}
.ci-error {
margin-right: $btn-side-margin;
}
}
.mr-widget-body,
......
......@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
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
before_action :authorize_read_issue!, only: [:show, :rendered_title]
before_action :authorize_read_issue!, only: [:show, :realtime_changes]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
......@@ -207,7 +207,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def rendered_title
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: {
......
......@@ -52,5 +52,15 @@ module EE
def admin_or_auditor?
admin? || auditor?
end
def remember_me!
return if ::Gitlab::Geo.secondary?
super
end
def forget_me!
return if ::Gitlab::Geo.secondary?
super
end
end
end
......@@ -312,7 +312,7 @@ class Issue < ActiveRecord::Base
end
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,
self
......
module Geo
class RepositoryBackfillService
class RepositorySyncService
attr_reader :project_id
LEASE_TIMEOUT = 8.hours.freeze
LEASE_KEY_PREFIX = 'repository_backfill_service'.freeze
LEASE_KEY_PREFIX = 'repository_sync_service'.freeze
def initialize(project_id)
@project_id = project_id
......@@ -81,7 +81,7 @@ module Geo
end
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.last_repository_synced_at = started_at
registry.last_repository_successful_sync_at = finished_at if finished_at
......
......@@ -3,6 +3,7 @@
- if project
:javascript
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = {
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)}",
......@@ -11,5 +12,3 @@
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])}"
};
gl.GfmAutoComplete.setup();
......@@ -2,8 +2,8 @@
%html{ lang: I18n.locale, class: "#{page_class}" }
= render "layouts/head"
%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/page', sidebar: sidebar, nav: nav
= yield :scripts_body
= render "layouts/init_auto_complete" if @gfm_form
......@@ -4,9 +4,9 @@
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('boards')
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'boards'
%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
......
......@@ -7,8 +7,9 @@
= render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('issues')
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
= webpack_bundle_tag 'issues'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
......
......@@ -51,7 +51,7 @@
.issue-details.issuable-details
.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' : '',
} }
.issue-title-entrypoint
......
......@@ -27,7 +27,8 @@
#js-vue-mr-widget.mr-widget
- 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
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......
......@@ -8,7 +8,8 @@
= render 'projects/last_push'
- 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?
%div{ class: container_class }
......
......@@ -23,7 +23,7 @@
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
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
.scroll-container
%ul.tokens-container.list-unstyled
......
class GeoBackfillWorker
class GeoRepositorySyncWorker
include Sidekiq::Worker
include CronjobQueue
......@@ -15,20 +15,20 @@ class GeoBackfillWorker
project_ids_updated_recently = find_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|
begin
break if over_time?(start_time)
break unless Gitlab::Geo.current_node_enabled?
# We try to obtain a lease here for the entire backfilling process
# because backfill the repositories continuously at a controlled rate
# instead of hammering the primary node. Initially, we are backfilling
# We try to obtain a lease here for the entire sync process because we
# want to sync the repositories continuously at a controlled rate
# 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
# minutes all of 100 projects will be synced.
try_obtain_lease do |lease|
Geo::RepositoryBackfillService.new(project_id).execute
Geo::RepositorySyncService.new(project_id).execute
end
rescue ActiveRecord::RecordNotFound
logger.error("Couldn't find project with ID=#{project_id}, skipping syncing")
......@@ -36,7 +36,7 @@ class GeoBackfillWorker
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
private
......@@ -86,10 +86,10 @@ class GeoBackfillWorker
end
def lease_key
Geo::RepositoryBackfillService::LEASE_KEY_PREFIX
Geo::RepositorySyncService::LEASE_KEY_PREFIX
end
def lease_timeout
Geo::RepositoryBackfillService::LEASE_TIMEOUT
Geo::RepositorySyncService::LEASE_TIMEOUT
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
geo_bulk_notify_worker:
cron: "*/10 * * * * *"
# GitLab Geo backfill worker
# GitLab Geo repository sync worker
# NOTE: This will only take effect if Geo is enabled
geo_backfill_worker:
geo_repository_sync_worker:
cron: "*/5 * * * *"
# GitLab Geo file download worker
# GitLab Geo file download dispatch worker
# NOTE: This will only take effect if Geo is enabled
geo_download_dispatch_worker:
geo_file_download_dispatch_worker:
cron: "*/10 * * * *"
registry:
......
......@@ -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']['cron'] ||= '*/10 * * * * *'
Settings.cron_jobs['geo_bulk_notify_worker']['job_class'] ||= 'GeoBulkNotifyWorker'
Settings.cron_jobs['geo_backfill_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_backfill_worker']['cron'] ||= '*/5 * * * *'
Settings.cron_jobs['geo_backfill_worker']['job_class'] ||= 'GeoBackfillWorker'
Settings.cron_jobs['geo_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_download_dispatch_worker']['cron'] ||= '5 * * * *'
Settings.cron_jobs['geo_download_dispatch_worker']['job_class'] ||= 'GeoFileDownloadDispatchWorker'
Settings.cron_jobs['geo_repository_sync_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_repository_sync_worker']['cron'] ||= '*/5 * * * *'
Settings.cron_jobs['geo_repository_sync_worker']['job_class'] ||= 'GeoRepositorySyncWorker'
Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '5 * * * *'
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']['cron'] ||= Settings.send(:cron_random_weekly_time)
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
......
......@@ -279,7 +279,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :rendered_title
get :realtime_changes
post :create_merge_request
end
collection do
......
......@@ -143,16 +143,24 @@ var config = {
'diff_notes',
'environments',
'environments_folder',
'sidebar',
'filtered_search',
'issue_show',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
'pipelines',
<<<<<<< HEAD
'mr_widget_ee',
'issue_show',
'balsamiq_viewer',
'pipelines_graph',
=======
'pipelines_graph',
'schedule_form',
'schedules_index',
'sidebar',
'vue_merge_request_widget',
>>>>>>> origin/master
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
......
......@@ -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.
Currently, the following tests are in place:
1. `docs:check:links`: Check that all internal (relative) links work correctly
1. `docs:check:apilint`: Check that the API docs follow some conventions
1. `docs lint`: Check that all internal (relative) links work correctly and
that all cURL examples in API docs use the full switches.
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,
a valid name would be `docs/update-api-issues` and it will run only the docs
tests. If the name is `docs-update-api-issues`, the whole test suite will run
(including docs).
the CI process by following some branch naming conventions. You have three
choices:
| 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.
### 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.
Depending on the number of required approvals and the number of approvers set,
......@@ -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
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.
......
......@@ -7,8 +7,8 @@ module Gitlab
# - Don't contain a reserved word (expect for the words used in the
# regex itself)
# - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
# - Ending in `issues/id`/rendered_title` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues rendered_title
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
......@@ -18,7 +18,7 @@ module Gitlab
'issue_notes'
),
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'
),
Gitlab::EtagCaching::Router::Route.new(
......
......@@ -13,7 +13,7 @@ module Gitlab
).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
self.cache_value(:geo_node_current) do
......@@ -37,7 +37,7 @@ module Gitlab
def self.current_node_enabled?
# 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.
Gitlab::Geo.current_node.reload.enabled?
end
......@@ -74,12 +74,12 @@ module Gitlab
Sidekiq::Cron::Job.find('geo_bulk_notify_worker')
end
def self.backfill_job
Sidekiq::Cron::Job.find('geo_backfill_worker')
def self.repository_sync_job
Sidekiq::Cron::Job.find('geo_repository_sync_worker')
end
def self.file_download_job
Sidekiq::Cron::Job.find('geo_download_dispatch_worker')
Sidekiq::Cron::Job.find('geo_file_download_dispatch_worker')
end
def self.configure_primary_jobs!
......
......@@ -3,17 +3,17 @@ require 'spec_helper'
describe 'Recent searches', js: true, feature: true do
include FilteredSearchHelpers
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let(:project_1) { create(:empty_project, :public) }
let(:project_2) { create(:empty_project, :public) }
let(:project_1_local_storage_key) { "#{project_1.full_path}-issue-recent-searches" }
before do
Capybara.ignore_hidden_elements = false
project.add_master(user)
group.add_developer(user)
create(:issue, project: project)
login_as(user)
create(:issue, project: project_1)
create(:issue, project: project_2)
# Visit any fast-loading page so we can clear local storage without a DOM exception
visit '/404'
remove_recent_searches
end
......@@ -22,7 +22,7 @@ describe 'Recent searches', js: true, feature: true do
end
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('bar', submit: true)
......@@ -35,8 +35,8 @@ describe 'Recent searches', js: true, feature: true do
end
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.namespace, project, label_name: 'qux', search: 'garply')
visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'foo', search: 'bar')
visit namespace_project_issues_path(project_1.namespace, project_1, label_name: 'qux', search: 'garply')
items = all('.filtered-search-history-dropdown-item', visible: false)
......@@ -46,9 +46,9 @@ describe 'Recent searches', js: true, feature: true do
end
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)
......@@ -58,9 +58,27 @@ describe 'Recent searches', js: true, feature: true do
expect(items[2].text).to eq('saved2')
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
set_recent_searches('["foo", "bar"]')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, '["foo", "bar"]')
visit namespace_project_issues_path(project_1.namespace, project_1)
all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click')
wait_for_filtered_search('foo')
......@@ -69,8 +87,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'clear recent searches button, clears recent searches' do
set_recent_searches('["foo"]')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, '["foo"]')
visit namespace_project_issues_path(project_1.namespace, project_1)
items_before = all('.filtered-search-history-dropdown-item', visible: false)
......@@ -83,8 +101,8 @@ describe 'Recent searches', js: true, feature: true do
end
it 'shows flash error when failed to parse saved history' do
set_recent_searches('fail')
visit namespace_project_issues_path(project.namespace, project)
set_recent_searches(project_1_local_storage_key, 'fail')
visit namespace_project_issues_path(project_1.namespace, project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches')
end
......
......@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
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
it 'loads on new issue page' do
......
/* eslint no-param-reassign: "off" */
require('~/gfm_auto_complete');
import GfmAutoComplete from '~/gfm_auto_complete';
require('vendor/jquery.caret');
require('vendor/jquery.atwho');
const global = window.gl || (window.gl = {});
const GfmAutoComplete = global.GfmAutoComplete;
describe('GfmAutoComplete', function () {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
fetchData: () => {},
});
describe('DefaultOptions.sorter', function () {
describe('assets loading', function () {
beforeEach(function () {
......@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () {
this.atwhoInstance = { setting: {} };
this.items = [];
this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
this.sorterValue = gfmAutoCompleteCallbacks.sorter
.call(this.atwhoInstance, '', this.items);
});
......@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance);
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
......@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () {
it('should enable highlightFirst if a query is present', function () {
const atwhoInstance = { setting: {} };
GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query');
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
......@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () {
const items = [];
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);
});
......@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () {
describe('DefaultOptions.matcher', function () {
const defaultMatcher = (context, flag, subtext) => (
GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext)
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext)
);
const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%'];
......
......@@ -35,7 +35,7 @@ describe('Issue Title', () => {
const issueShowComponent = new IssueTitleDescriptionComponent({
propsData: {
canUpdateIssue: '.css-stuff',
endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
},
}).$mount();
......
......@@ -377,7 +377,7 @@ import '~/notes';
});
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);
expect(hasSlashCommands).toBeTruthy();
......@@ -401,10 +401,18 @@ import '~/notes';
describe('stripSlashCommands', () => {
it('should strip slash commands from the comment which begins with a slash command', () => {
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);
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', () => {
......@@ -424,6 +432,22 @@ import '~/notes';
beforeEach(() => {
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', () => {
......@@ -444,7 +468,21 @@ import '~/notes';
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
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', () => {
......
......@@ -14,7 +14,7 @@ describe Gitlab::EtagCaching::Router do
it 'matches issue title endpoint' do
env = build_env(
'/my-group/my-project/issues/123/rendered_title'
'/my-group/my-project/issues/123/realtime_changes'
)
result = described_class.match(env)
......
......@@ -122,11 +122,8 @@ describe Gitlab::Geo, lib: true do
end
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) }
# TODO: Make this name consistent
init_cron_job('geo_download_dispatch_worker', 'GeoFileDownloadDispatchWorker')
end
it 'activates cron jobs for primary' do
......@@ -134,7 +131,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs!
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
end
......@@ -143,7 +140,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs!
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
end
......@@ -151,7 +148,7 @@ describe Gitlab::Geo, lib: true do
described_class.configure_cron_jobs!
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
end
end
......
......@@ -1967,4 +1967,36 @@ describe User, models: true do
expect(user.preferred_language).to eq('en')
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
......@@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do
)
end
it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(pipeline).to be_valid }
it { expect(pipeline).to eq(project.pipelines.last) }
it { expect(pipeline).to have_attributes(user: user) }
it { expect(pipeline).to have_attributes(status: 'pending') }
it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
it 'creates a pipeline' do
expect(pipeline).to be_kind_of(Ci::Pipeline)
expect(pipeline).to be_valid
expect(pipeline).to eq(project.pipelines.last)
expect(pipeline).to have_attributes(user: user)
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
it 'updates head pipeline of each merge request' do
......
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') }
subject { described_class.new(project.id) }
......@@ -106,7 +106,7 @@ describe Geo::RepositoryBackfillService, services: true do
end
end
context 'when repository was backfilled successfully' do
context 'when repository was synced successfully' do
let(:project) { create(:project) }
let(:last_repository_synced_at) { 5.days.ago }
......@@ -159,7 +159,7 @@ describe Geo::RepositoryBackfillService, services: true do
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!(:registry) do
......
......@@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do
end
end
it { expect(issue).to be_valid }
it { expect(issue).to be_closed }
it 'closes the issue' do
expect(issue).to be_valid
expect(issue).to be_closed
end
it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
......@@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do
described_class.new(project, user).close_issue(issue)
end
it { expect(issue).to be_valid }
it { expect(issue).to be_opened }
it { expect(todo.reload).to be_pending }
it 'closes the issue' do
expect(issue).to be_valid
expect(issue).to be_opened
expect(todo.reload).to be_pending
end
end
end
end
......@@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do
@merge_request = service.execute
end
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.title).to eq('Awesome merge_request') }
it { expect(@merge_request.assignee).to be_nil }
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'creates an MR' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('Awesome merge_request')
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
expect(service).to have_received(:execute_hooks).with(@merge_request)
......
......@@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do
end
end
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.title).to eq('New title') }
it { expect(@merge_request.assignee).to eq(user2) }
it { expect(@merge_request).to be_closed }
it { expect(@merge_request.labels.count).to eq(1) }
it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') }
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'mathces base expectations' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
expect(@merge_request.assignee).to eq(user2)
expect(@merge_request).to be_closed
expect(@merge_request.labels.count).to eq(1)
expect(@merge_request.labels.first.title).to eq(label.name)
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
expect(service).to have_received(:execute_hooks).
......@@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.state).to eq('merged') }
it { expect(@merge_request.merge_error).to be_nil }
it 'merges the MR' do
expect(@merge_request).to be_valid
expect(@merge_request.state).to eq('merged')
expect(@merge_request.merge_error).to be_nil
end
end
context 'with finished pipeline' do
......@@ -167,8 +171,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.state).to eq('merged') }
it 'merges the MR' do
expect(@merge_request).to be_valid
expect(@merge_request.state).to eq('merged')
end
end
context 'with active pipeline' do
......@@ -202,8 +208,10 @@ describe MergeRequests::UpdateService, services: true do
end
end
it { expect(@merge_request.state).to eq('opened') }
it { expect(@merge_request.merge_error).not_to be_nil }
it 'does not merge the MR' do
expect(@merge_request.state).to eq('opened')
expect(@merge_request.merge_error).not_to be_nil
end
end
context 'MR can not be merged when note sha != MR sha' do
......
......@@ -73,11 +73,11 @@ module FilteredSearchHelpers
end
def remove_recent_searches
execute_script('window.localStorage.removeItem(\'issue-recent-searches\');')
execute_script('window.localStorage.clear();')
end
def set_recent_searches(input)
execute_script("window.localStorage.setItem('issue-recent-searches', '#{input}');")
def set_recent_searches(key, input)
execute_script("window.localStorage.setItem('#{key}', '#{input}');")
end
def wait_for_filtered_search(text)
......
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!(:secondary) { create(:geo_node, :current) }
let!(:project_1) { create(:empty_project) }
......@@ -13,25 +13,25 @@ describe Geo::GeoBackfillWorker, services: true do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { true }
end
it 'performs Geo::RepositoryBackfillService for each project' do
expect(Geo::RepositoryBackfillService).to receive(:new).twice.and_return(spy)
it 'performs Geo::RepositorySyncService for each project' do
expect(Geo::RepositorySyncService).to receive(:new).twice.and_return(spy)
subject.perform
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(
project: project_1,
last_repository_synced_at: DateTime.now,
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
end
it 'performs Geo::RepositoryBackfillService for backfilled projects updated recently' do
it 'performs Geo::RepositorySyncService for synced projects updated recently' do
Geo::ProjectRegistry.create(
project: project_1,
last_repository_synced_at: 2.days.ago,
......@@ -47,39 +47,39 @@ describe Geo::GeoBackfillWorker, services: true do
project_1.update_attribute(:last_repository_updated_at, 2.days.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
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 }
expect(Geo::RepositoryBackfillService).not_to receive(:new)
expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform
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 }
expect(Geo::RepositoryBackfillService).not_to receive(:new)
expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform
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 }
expect(Geo::RepositoryBackfillService).not_to receive(:new)
expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform
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 }
expect(Geo::RepositoryBackfillService).not_to receive(:new)
expect(Geo::RepositorySyncService).not_to receive(:new)
subject.perform
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