Commit d08b1ed8 authored by Lin Jen-Shin's avatar Lin Jen-Shin

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

* ee/master:
  Linter fixes
  Test PostgreSQL 9.6 and postgres_fdw with Geo specs
  Move line code generation into Gitlab::Git
  Create a Gitlab::Git submodule for conlict-related files
  Refactor conflict resolution to contain git ops within Gitlab::Git
  Clean hierarchy of calls between models and Gitalb::Git for blob search
  Move Gitlab::Diff::LineCode to module Gitlab::Git
  Encapsulate git operations for conflict resolution into lib
  Popen with a timeout
  EE version of ph-shortcut-js-modules
  Memoize migration files per migrations paths
  Remove duplicated sections
  Remove +x permission from app/assets/images/favicon-green.ico
  added date parameter for time tracking
  add spec for projects
  tests for sidebar labels
  fix creating labels from Group Boards sidebar
parents 599472ac 396926be
......@@ -69,6 +69,12 @@ stages:
- redis:alpine
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.use-pg-9-6: &use-pg-9-6
services:
- postgres:9.6
- redis:alpine
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
.use-mysql: &use-mysql
services:
- mysql:latest
......@@ -117,6 +123,17 @@ stages:
<<: *use-pg
<<: *except-docs
.rspec-geo-pg-9-6: &rspec-metadata-pg-geo
<<: *use-pg-9-6
<<: *except-docs
stage: test
script:
- export NO_KNAPSACK=1
- export CACHE_CLASSES=true
- source scripts/prepare_postgres_fdw.sh
- scripts/gitaly-test-spawn
- bundle exec rspec --color --format documentation --tag geo spec/
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
......@@ -338,6 +355,8 @@ rspec-pg 22 25: *rspec-metadata-pg
rspec-pg 23 25: *rspec-metadata-pg
rspec-pg 24 25: *rspec-metadata-pg
rspec-pg geo: *rspec-metadata-pg-geo
rspec-mysql 0 25: *rspec-metadata-mysql
rspec-mysql 1 25: *rspec-metadata-mysql
rspec-mysql 2 25: *rspec-metadata-mysql
......
File mode changed from 100755 to 100644
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global ProjectSelect */
/* global ShortcutsNavigation */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global Milestone */
/* global IssuableForm */
/* global LabelsSelect */
......@@ -32,13 +30,9 @@ import CILintEditor from './ci_lint_editor';
/* global ProjectImport */
import Labels from './labels';
import LabelManager from './label_manager';
/* global Shortcuts */
/* global ShortcutsFindFile */
/* global Sidebar */
/* global WeightSelect */
/* global AdminEmailSelect */
/* global ShortcutsWiki */
import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
......@@ -87,6 +81,10 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form';
import Shortcuts from './shortcuts';
import ShortcutsNavigation from './shortcuts_navigation';
import ShortcutsFindFile from './shortcuts_find_file';
import ShortcutsIssuable from './shortcuts_issuable';
import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
......
......@@ -21,15 +21,6 @@ window._ = _;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
// shortcuts
import './shortcuts';
import './shortcuts_blob';
import './shortcuts_dashboard_navigation';
import './shortcuts_navigation';
import './shortcuts_find_file';
import './shortcuts_issuable';
import './shortcuts_network';
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
/* global ShortcutsNetwork */
import ShortcutsNetwork from '../shortcuts_network';
import Network from './network';
$(function() {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() {
this.Shortcuts = (function() {
function Shortcuts(skipResetBindings) {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar);
const findFileURL = document.body.dataset.findFile;
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
return gl.utils.visitUrl(findFileURL);
});
}
const defaultStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
}
return defaultStopCallback(e, element, combo);
};
export default class Shortcuts {
constructor(skipResetBindings) {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
Shortcuts.prototype.onToggleHelp = function(e) {
e.preventDefault();
return Shortcuts.toggleHelp(this.enabledHelp);
};
const findFileURL = document.body.dataset.findFile;
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview);
Shortcuts.prototype.onTogglePerfBar = function(e) {
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
Mousetrap.bind('t', () => {
gl.utils.visitUrl(findFileURL);
});
}
$(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
$(this).remove();
$('.hidden-shortcut').show();
e.preventDefault();
const performanceBarCookieName = 'perf_bar_enabled';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
};
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
Shortcuts.toggleHelp = function(location) {
var $modal;
$modal = $('#modal-shortcuts');
if ($modal.length) {
$modal.modal('toggle');
return;
}
return $.ajax({
url: gon.shortcuts_path,
dataType: 'script',
success: function(e) {
var i, l, len, results;
if (location && location.length > 0) {
results = [];
for (i = 0, len = location.length; i < len; i += 1) {
l = location[i];
results.push($(l).show());
}
return results;
} else {
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
});
}
onToggleHelp(e) {
e.preventDefault();
Shortcuts.toggleHelp(this.enabledHelp);
}
static onTogglePerfBar(e) {
e.preventDefault();
const performanceBarCookieName = 'perf_bar_enabled';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
}
static toggleMarkdownPreview(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
}
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
static toggleHelp(location) {
const $modal = $('#modal-shortcuts');
if ($modal.length) {
$modal.modal('toggle');
}
$.ajax({
url: gon.shortcuts_path,
dataType: 'script',
success() {
if (location && location.length > 0) {
const results = [];
for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show());
}
return results;
}
});
};
Shortcuts.prototype.focusFilter = function(e) {
if (this.filterInput == null) {
this.filterInput = $('input[type=search]', '.nav-controls');
}
this.filterInput.focus();
return e.preventDefault();
};
Shortcuts.focusSearch = function(e) {
$('#search').focus();
return e.preventDefault();
};
return Shortcuts;
})();
$(document).on('click.more_help', '.js-more-help-button', function(e) {
$(this).remove();
$('.hidden-shortcut').show();
return e.preventDefault();
});
Mousetrap.stopCallback = (function() {
var defaultStopCallback;
defaultStopCallback = Mousetrap.stopCallback;
return function(e, element, combo) {
// allowed shortcuts if textarea, input, contenteditable are focused
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
} else {
return defaultStopCallback.apply(this, arguments);
}
};
})();
}).call(window);
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
},
});
}
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
}
this.filterInput.focus();
e.preventDefault();
}
static focusSearch(e) {
$('#search').focus();
e.preventDefault();
}
}
/* global Mousetrap */
/* global Shortcuts */
import './shortcuts';
import Shortcuts from './shortcuts';
const defaults = {
skipResetBindings: false,
......
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
/* global Mousetrap */
/* global ShortcutsNavigation */
import './shortcuts_navigation';
import ShortcutsNavigation from './shortcuts_navigation';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
export default class ShortcutsFindFile extends ShortcutsNavigation {
constructor(projectFindFile) {
super();
this.ShortcutsFindFile = (function(superClass) {
extend(ShortcutsFindFile, superClass);
const oldStopCallback = Mousetrap.stopCallback;
this.projectFindFile = projectFindFile;
function ShortcutsFindFile(projectFindFile) {
var _oldStopCallback;
this.projectFindFile = projectFindFile;
ShortcutsFindFile.__super__.constructor.call(this);
_oldStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (function(_this) {
// override to fire shortcuts action when focus in textbox
return function(event, element, combo) {
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
// when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
return _oldStopCallback(event, element, combo);
};
})(this);
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
Mousetrap.bind('esc', this.projectFindFile.goToTree);
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
}
Mousetrap.stopCallback = (e, element, combo) => {
if (
element === this.projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
// when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
return ShortcutsFindFile;
})(ShortcutsNavigation);
}).call(window);
return oldStopCallback(e, element, combo);
};
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
Mousetrap.bind('esc', this.projectFindFile.goToTree);
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
}
}
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
/* global ShortcutsNavigation */
/* global sidebar */
import _ from 'underscore';
import 'mousetrap';
import './shortcuts_navigation';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.ShortcutsIssuable = (function(superClass) {
extend(ShortcutsIssuable, superClass);
function ShortcutsIssuable(isMergeRequest) {
ShortcutsIssuable.__super__.constructor.call(this);
Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee'));
Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
Mousetrap.bind('r', (function(_this) {
return function() {
_this.replyWithSelectedText(isMergeRequest);
return false;
};
})(this));
Mousetrap.bind('e', (function(_this) {
return function() {
_this.editIssue();
return false;
};
})(this));
Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels'));
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
} else {
this.enabledHelp.push('.hidden-shortcut.issues');
}
import ShortcutsNavigation from './shortcuts_navigation';
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
this.editBtn = document.querySelector('.issuable-edit');
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
Mousetrap.bind('e', this.editIssue.bind(this));
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
} else {
this.enabledHelp.push('.hidden-shortcut.issues');
}
}
replyWithSelectedText() {
const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) {
this.$replyField.focus();
return false;
}
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = window.gl.CopyAsGFM.nodeToGFM(el);
ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) {
var quote, documentFragment, el, selected, separator;
let replyField;
if (isMergeRequest) {
replyField = $('.js-main-target-form #note_note');
} else {
replyField = $('.js-main-target-form .js-vue-comment-form');
}
documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) {
replyField.focus();
return;
}
el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
selected = window.gl.CopyAsGFM.nodeToGFM(el);
if (selected.trim() === "") {
return;
}
quote = _.map(selected.split("\n"), function(val) {
return ("> " + val).trim() + "\n";
});
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n\n" || '';
replyField.val(function(a, current) {
return current + separator + quote.join('') + "\n";
});
// Trigger autosave
replyField.trigger('input').trigger('change');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
replyField.get(0).dispatchEvent(event);
// Focus the input field
return replyField.focus();
};
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
$editBtn.get(0).click();
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
sidebar.openDropdown(name);
if (selected.trim() === '') {
return false;
};
}
const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
// If replyField already has some content, add a newline before our quote
const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
.trigger('input')
.trigger('change');
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
this.$replyField.get(0).dispatchEvent(event);
// Focus the input field
this.$replyField.focus();
return false;
}
editIssue() {
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
this.editBtn.click();
return false;
}
return ShortcutsIssuable;
})(ShortcutsNavigation);
}).call(window);
static openSidebarDropdown(name) {
sidebar.openDropdown(name);
return false;
}
}
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
import findAndFollowLink from './shortcuts_dashboard_navigation';
import './shortcuts';
import Shortcuts from './shortcuts';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
export default class ShortcutsNavigation extends Shortcuts {
constructor() {
super();
this.ShortcutsNavigation = (function(superClass) {
extend(ShortcutsNavigation, superClass);
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
function ShortcutsNavigation() {
ShortcutsNavigation.__super__.constructor.call(this);
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
}
return ShortcutsNavigation;
})(Shortcuts);
}).call(window);
this.enabledHelp.push('.hidden-shortcut.project');
}
}
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
/* global Mousetrap */
/* global ShortcutsNavigation */
import ShortcutsNavigation from './shortcuts_navigation';
import './shortcuts_navigation';
export default class ShortcutsNetwork extends ShortcutsNavigation {
constructor(graph) {
super();
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
Mousetrap.bind(['left', 'h'], graph.scrollLeft);
Mousetrap.bind(['right', 'l'], graph.scrollRight);
Mousetrap.bind(['up', 'k'], graph.scrollUp);
Mousetrap.bind(['down', 'j'], graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
this.ShortcutsNetwork = (function(superClass) {
extend(ShortcutsNetwork, superClass);
function ShortcutsNetwork(graph) {
this.graph = graph;
ShortcutsNetwork.__super__.constructor.call(this);
Mousetrap.bind(['left', 'h'], this.graph.scrollLeft);
Mousetrap.bind(['right', 'l'], this.graph.scrollRight);
Mousetrap.bind(['up', 'k'], this.graph.scrollUp);
Mousetrap.bind(['down', 'j'], this.graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom);
this.enabledHelp.push('.hidden-shortcut.network');
}
return ShortcutsNetwork;
})(ShortcutsNavigation);
}).call(window);
this.enabledHelp.push('.hidden-shortcut.network');
}
}
/* eslint-disable class-methods-use-this */
/* global Mousetrap */
/* global ShortcutsNavigation */
import ShortcutsNavigation from './shortcuts_navigation';
import findAndFollowLink from './shortcuts_dashboard_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
......
......@@ -53,7 +53,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Conflict::ResolutionError => e
rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
......
......@@ -9,7 +9,7 @@ module TimeTrackable
extend ActiveSupport::Concern
included do
attr_reader :time_spent, :time_spent_user
attr_reader :time_spent, :time_spent_user, :spent_at
alias_method :time_spent?, :time_spent
......@@ -24,6 +24,7 @@ module TimeTrackable
def spend_time(options)
@time_spent = options[:duration]
@time_spent_user = options[:user]
@spent_at = options[:spent_at]
@original_total_time_spent = nil
return if @time_spent == 0
......@@ -55,7 +56,11 @@ module TimeTrackable
end
def add_or_subtract_spent_time
timelogs.new(time_spent: time_spent, user: @time_spent_user)
timelogs.new(
time_spent: time_spent,
user: @time_spent_user,
spent_at: @spent_at
)
end
def check_negative_time_spent
......
......@@ -475,9 +475,7 @@ class Repository
end
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project)
end
Blob.decorate(raw_repository.blob_at(sha, path), project)
rescue Gitlab::Git::Repository::NoRepository
nil
end
......@@ -921,14 +919,6 @@ class Repository
end
end
def resolve_conflicts(user, branch_name, params)
with_branch(user, branch_name) do
committer = user_to_committer(user)
create_commit(params.merge(author: committer, committer: committer))
end
end
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
......@@ -1188,7 +1178,7 @@ class Repository
def last_commit_id_for_path_by_shelling_out(sha, path)
args = %W(rev-list --max-count=1 #{sha} -- #{path})
run_git(args).first.strip
raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
end
def repository_storage_path
......
......@@ -23,13 +23,13 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing
rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
def conflicts
@conflicts ||= Gitlab::Conflict::FileCollection.read_only(merge_request)
@conflicts ||= Gitlab::Conflict::FileCollection.new(merge_request)
end
end
end
......
module MergeRequests
module Conflicts
class ResolveService < MergeRequests::Conflicts::BaseService
MissingFiles = Class.new(Gitlab::Conflict::ResolutionError)
def execute(current_user, params)
rugged = merge_request.source_project.repository.rugged
Gitlab::Conflict::FileCollection.for_resolution(merge_request) do |conflicts_for_resolution|
merge_index = conflicts_for_resolution.merge_index
params[:files].each do |file_params|
conflict_file = conflicts_for_resolution.file_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(merge_index, rugged, conflict_file, file_params)
end
unless merge_index.conflicts.empty?
missing_files = merge_index.conflicts.map { |file| file[:ours][:path] }
raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
message: params[:commit_message] || conflicts_for_resolution.default_commit_message,
parents: [conflicts_for_resolution.our_commit, conflicts_for_resolution.their_commit].map(&:oid),
tree: merge_index.write_tree(rugged)
}
conflicts_for_resolution
.project
.repository
.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
end
end
private
def write_resolved_file_to_index(merge_index, rugged, file, params)
if params[:sections]
new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n")
new_file << "\n" if file.our_blob.data.ends_with?("\n")
elsif params[:content]
new_file = file.resolve_content(params[:content])
end
our_path = file.our_path
conflicts = Gitlab::Conflict::FileCollection.new(merge_request)
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
merge_index.conflict_remove(our_path)
conflicts.resolve(current_user, params[:commit_message], params[:files])
end
end
end
......
......@@ -382,7 +382,7 @@ module QuickActions
end
desc 'Add or substract spent time'
explanation do |time_spent|
explanation do |time_spent, time_spent_date|
if time_spent
if time_spent > 0
verb = 'Adds'
......@@ -395,16 +395,20 @@ module QuickActions
"#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
end
end
params '<1h 30m | -1h 30m>'
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_duration|
Gitlab::TimeTrackingFormatter.parse(raw_duration)
parse_params do |raw_time_date|
Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
end
command :spend do |time_spent|
command :spend do |time_spent, time_spent_date|
if time_spent
@updates[:spend_time] = { duration: time_spent, user: current_user }
@updates[:spend_time] = {
duration: time_spent,
user: current_user,
spent_at: time_spent_date
}
end
end
......
......@@ -195,9 +195,11 @@ module SystemNoteService
if time_spent == :reset
body = "removed time spent"
else
spent_at = noteable.spent_at
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
body = "#{action} #{parsed_time} of time spent"
body << " at #{spent_at}" if spent_at
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
......@@ -25,7 +25,7 @@
show_any: "true",
project_id: @project&.try(:id),
labels: labels_filter_path(false),
namespace_path: @project.try(:namespace).try(:full_path),
namespace_path: @namespace_path,
project_path: @project.try(:path) },
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text
......
---
title: Added possibility to enter past date in /spend command to log time in the past
merge_request: 3044
author: g3dinua, LockiStrike
type: changed
---
title: Use a timeout on certain git operations
merge_request: 14872
author:
type: security
......@@ -10,7 +10,8 @@ if Rails.env.test?
# it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5
# seconds per spec.
def migrations(paths)
(@migrations ||= migrations_unmemoized(paths)).dup
@migrations ||= {}
(@migrations[paths] ||= migrations_unmemoized(paths)).dup
end
end
end
......
class AddSpentAtToTimelogs < ActiveRecord::Migration
DOWNTIME = false
def up
add_column :timelogs, :spent_at, :datetime_with_timezone
end
def down
remove_column :timelogs, :spent_at
end
end
......@@ -1907,6 +1907,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do
t.datetime "updated_at", null: false
t.integer "issue_id"
t.integer "merge_request_id"
t.datetime_with_timezone "spent_at"
end
add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
......
......@@ -190,43 +190,6 @@ load and will have a corresponding badge counter to match the counter on the ima
![Image resolved discussion](img/image_resolved_discussion.png)
## Locking discussions
> [Introduced][ce-14531] in GitLab 10.1.
Sometimes a discussion is revolved around an image. With image discussions,
you can easily target a specific coordinate of an image and start a discussion
around it. Image discussions are available in merge requests and commit detail views.
To start an image discussion, hover your mouse over the image. Your mouse pointer
should convert into an icon, indicating that the image is available for commenting.
Simply click anywhere on the image to create a new discussion.
![Start image discussion](img/start_image_discussion.gif)
After you click on the image, a comment form will be displayed that would be the start
of your discussion. Once you save your comment, you will see a new badge displayed on
top of your image. This badge represents your discussion.
>**Note:**
This discussion badge is typically associated with a number that is only used as a visual
reference for each discussion. In the merge request discussion tab,
this badge will be indicated with a comment icon since each discussion will render a new
image section.
Image discussions also work on diffs that replace an existing image. In this diff view
mode, you can toggle the different view modes and still see the discussion point badges.
| 2-up | Swipe | Onion Skin |
| :-----------: | :----------: | :----------: |
| ![2-up view](img/two_up_view.png) | ![swipe view](img/swipe_view.png) | ![onion skin view](img/onion_skin_view.png) |
Image discussions also work well with resolvable discussions. Resolved discussions
on diffs (not on the merge request discussion tab) will appear collapsed on page
load and will have a corresponding badge counter to match the counter on the image.
![Image resolved discussion](img/image_resolved_discussion.png)
## Lock discussions
> [Introduced][ce-14531] in GitLab 10.1.
......
......@@ -33,7 +33,7 @@ do.
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
| <code>/spend &lt;time(1h 30m &#124; -1h 5m)&gt; &lt;date(YYYY-MM-DD)&gt;</code> | Add or subtract spent time; optionally, specify the date that time was spent on |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
......
......@@ -186,7 +186,7 @@ module API
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
......
......@@ -173,7 +173,7 @@ module API
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
......
......@@ -23,7 +23,7 @@ module Github
private
def generate_line_code(line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end
def on_diff?
......
......@@ -241,7 +241,7 @@ module Gitlab
end
def generate_line_code(pr_comment)
Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
Gitlab::Git.diff_line_code(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
end
def pull_request_comment_attributes(comment)
......
......@@ -4,82 +4,29 @@ module Gitlab
include Gitlab::Routing
include IconsHelper
MissingResolution = Class.new(ResolutionError)
CONTEXT_LINES = 3
attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
def initialize(merge_file_result, conflict, merge_request:)
@merge_file_result = merge_file_result
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@our_mode = conflict[:ours][:mode]
@merge_request = merge_request
@repository = merge_request.project.repository
@match_line_headers = {}
end
def content
merge_file_result[:data]
end
attr_reader :merge_request
def our_blob
@our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
end
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
attr_reader :raw
def type
lines unless @type
delegate :type, :content, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
@type.inquiry
def initialize(raw, merge_request:)
@raw = raw
@merge_request = merge_request
@match_line_headers = {}
end
# Array of Gitlab::Diff::Line objects
def lines
return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Conflict::Parser.new.parse(content,
our_path: our_path,
their_path: their_path,
parent_file: self)
rescue Gitlab::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
@lines = raw.lines.nil? ? nil : map_raw_lines(raw.lines)
end
def resolve_lines(resolution)
section_id = nil
lines.map do |line|
unless line.type
section_id = nil
next line
end
section_id ||= line_code(line)
case resolution[section_id]
when 'head'
next unless line.type == 'new'
when 'origin'
next unless line.type == 'old'
else
raise MissingResolution, "Missing resolution for section ID: #{section_id}"
end
line
end.compact
end
def resolve_content(resolution)
if resolution == content
raise MissingResolution, "Resolved content has no changes for file #{our_path}"
end
resolution
map_raw_lines(raw.resolve_lines(resolution))
end
def highlight_lines!
......@@ -163,7 +110,7 @@ module Gitlab
end
def line_code(line)
Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
Gitlab::Git.diff_line_code(our_path, line.new_pos, line.old_pos)
end
def create_match_line(line)
......@@ -227,15 +174,14 @@ module Gitlab
new_path: our_path)
end
# Don't try to print merge_request or repository.
def inspect
instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
value = instance_variable_get("@#{instance_variable}")
private
"#{instance_variable}=\"#{value}\""
def map_raw_lines(raw_lines)
raw_lines.map do |raw_line|
Gitlab::Diff::Line.new(raw_line[:full_line], raw_line[:type],
raw_line[:line_obj_index], raw_line[:line_old],
raw_line[:line_new], parent_file: self)
end
"#<#{self.class} #{instance_variables.join(' ')}>"
end
end
end
......
module Gitlab
module Conflict
class FileCollection
ConflictSideMissing = Class.new(StandardError)
attr_reader :merge_request, :our_commit, :their_commit, :project
delegate :repository, to: :project
class << self
# We can only write when getting the merge index from the source
# project, because we will write to that project. We don't use this all
# the time because this fetches a ref into the source project, which
# isn't needed for reading.
def for_resolution(merge_request)
project = merge_request.source_project
new(merge_request, project).tap do |file_collection|
project
.repository
.with_repo_branch_commit(merge_request.target_project.repository.raw_repository, merge_request.target_branch) do
yield file_collection
end
end
end
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
def read_only(merge_request)
new(merge_request, merge_request.target_project)
end
attr_reader :merge_request, :resolver
def initialize(merge_request)
source_repo = merge_request.source_project.repository.raw
our_commit = merge_request.source_branch_head.raw
their_commit = merge_request.target_branch_head.raw
target_repo = merge_request.target_project.repository.raw
@resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit)
@merge_request = merge_request
end
def merge_index
@merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
def resolve(user, commit_message, files)
args = {
source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch,
commit_message: commit_message || default_commit_message
}
resolver.resolve_conflicts(user, files, args)
end
def files
@files ||= merge_index.conflicts.map do |conflict|
raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
conflict,
merge_request: merge_request)
@files ||= resolver.conflicts.map do |conflict_file|
Gitlab::Conflict::File.new(conflict_file, merge_request: merge_request)
end
end
......@@ -61,8 +42,8 @@ module Gitlab
end
def default_commit_message
conflict_filenames = merge_index.conflicts.map do |conflict|
"# #{conflict[:ours][:path]}"
conflict_filenames = files.map do |conflict|
"# #{conflict.our_path}"
end
<<EOM.chomp
......@@ -72,15 +53,6 @@ Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branc
#{conflict_filenames.join("\n")}
EOM
end
private
def initialize(merge_request, project)
@merge_request = merge_request
@our_commit = merge_request.source_branch_head.raw.rugged_commit
@their_commit = merge_request.target_branch_head.raw.rugged_commit
@project = project
end
end
end
end
module Gitlab
module Conflict
class Parser
UnresolvableError = Class.new(StandardError)
UnmergeableFile = Class.new(UnresolvableError)
UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
ParserError = Class.new(StandardError)
UnexpectedDelimiter = Class.new(ParserError)
MissingEndDelimiter = Class.new(ParserError)
def parse(text, our_path:, their_path:, parent_file: nil)
validate_text!(text)
line_obj_index = 0
line_old = 1
line_new = 1
type = nil
lines = []
conflict_start = "<<<<<<< #{our_path}"
conflict_middle = '======='
conflict_end = ">>>>>>> #{their_path}"
text.each_line.map do |line|
full_line = line.delete("\n")
if full_line == conflict_start
validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
type = 'nonewline'
lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
else
lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
line_old += 1 if type != 'new'
line_new += 1 if type != 'old'
line_obj_index += 1
end
end
raise MissingEndDelimiter unless type.nil?
lines
end
private
def validate_text!(text)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
text.force_encoding('UTF-8')
raise UnsupportedEncoding unless text.valid_encoding?
end
def validate_delimiter!(condition)
raise UnexpectedDelimiter unless condition
end
end
end
end
module Gitlab
module Conflict
ResolutionError = Class.new(StandardError)
end
end
......@@ -49,7 +49,7 @@ module Gitlab
def line_code(line)
return if line.meta?
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end
def line_for_line_code(code)
......
module Gitlab
module Diff
class LineCode
def self.generate(file_path, new_line_position, old_line_position)
"#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
end
end
end
end
......@@ -74,6 +74,18 @@ module Gitlab
GeoNode.where(host: host, port: port).exists?
end
def self.fdw?
self.cache_value(:geo_fdw?) do
::Geo::BaseRegistry.connection.execute(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '#{self.fdw_schema}' AND table_type = 'FOREIGN TABLE'"
).first.fetch('count').to_i.positive?
end
end
def self.fdw_schema
'gitlab_secondary'.freeze
end
def self.repository_sync_job
Sidekiq::Cron::Job.find('geo_repository_sync_worker')
end
......
......@@ -66,6 +66,10 @@ module Gitlab
end
end
end
def diff_line_code(file_path, new_line_position, old_line_position)
"#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
end
end
end
end
module Gitlab
module Git
module Conflict
class File
attr_reader :content, :their_path, :our_path, :our_mode, :repository
def initialize(repository, commit_oid, conflict, content)
@repository = repository
@commit_oid = commit_oid
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@our_mode = conflict[:ours][:mode]
@content = content
end
def lines
return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Git::Conflict::Parser.parse(content,
our_path: our_path,
their_path: their_path)
rescue Gitlab::Git::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
end
def type
lines unless @type
@type.inquiry
end
def our_blob
# REFACTOR NOTE: the source of `commit_oid` used to be
# `merge_request.diff_refs.head_sha`. Instead of passing this value
# around the new lib structure, I decided to use `@commit_oid` which is
# equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`.
# That is what `merge_request.diff_refs.head_sha` is equivalent to when
# `merge_request` is not persisted (see `MergeRequest#diff_head_commit`).
# I think using the same oid is more consistent anyways, but if Conflicts
# start breaking, the change described above is a good place to look at.
@our_blob ||= repository.blob_at(@commit_oid, our_path)
end
def line_code(line)
Gitlab::Git.diff_line_code(our_path, line[:line_new], line[:line_old])
end
def resolve_lines(resolution)
section_id = nil
lines.map do |line|
unless line[:type]
section_id = nil
next line
end
section_id ||= line_code(line)
case resolution[section_id]
when 'head'
next unless line[:type] == 'new'
when 'origin'
next unless line[:type] == 'old'
else
raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Missing resolution for section ID: #{section_id}"
end
line
end.compact
end
def resolve_content(resolution)
if resolution == content
raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Resolved content has no changes for file #{our_path}"
end
resolution
end
end
end
end
end
module Gitlab
module Git
module Conflict
class Parser
UnresolvableError = Class.new(StandardError)
UnmergeableFile = Class.new(UnresolvableError)
UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
ParserError = Class.new(StandardError)
UnexpectedDelimiter = Class.new(ParserError)
MissingEndDelimiter = Class.new(ParserError)
class << self
def parse(text, our_path:, their_path:, parent_file: nil)
validate_text!(text)
line_obj_index = 0
line_old = 1
line_new = 1
type = nil
lines = []
conflict_start = "<<<<<<< #{our_path}"
conflict_middle = '======='
conflict_end = ">>>>>>> #{their_path}"
text.each_line.map do |line|
full_line = line.delete("\n")
if full_line == conflict_start
validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
type = 'nonewline'
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: line_new
}
else
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: line_new
}
line_old += 1 if type != 'new'
line_new += 1 if type != 'old'
line_obj_index += 1
end
end
raise MissingEndDelimiter unless type.nil?
lines
end
private
def validate_text!(text)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
text.force_encoding('UTF-8')
raise UnsupportedEncoding unless text.valid_encoding?
end
def validate_delimiter!(condition)
raise UnexpectedDelimiter unless condition
end
end
end
end
end
end
module Gitlab
module Git
module Conflict
class Resolver
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
def initialize(repository, our_commit, target_repository, their_commit)
@repository = repository
@our_commit = our_commit.rugged_commit
@target_repository = target_repository
@their_commit = their_commit.rugged_commit
end
def conflicts
@conflicts ||= begin
target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit)
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
target_index.conflicts.map do |conflict|
raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
Gitlab::Git::Conflict::File.new(
@target_repository,
@our_commit.oid,
conflict,
target_index.merge_file(conflict[:ours][:path])[:data]
)
end
end
end
def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:)
@repository.with_repo_branch_commit(@target_repository, target_branch) do
files.each do |file_params|
conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(conflict_file, file_params)
end
unless index.conflicts.empty?
missing_files = index.conflicts.map { |file| file[:ours][:path] }
raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
message: commit_message,
parents: [@our_commit, @their_commit].map(&:oid)
}
@repository.commit_index(user, source_branch, index, commit_params)
end
end
def conflict_for_path(old_path, new_path)
conflicts.find do |conflict|
conflict.their_path == old_path && conflict.our_path == new_path
end
end
private
# We can only write when getting the merge index from the source
# project, because we will write to that project. We don't use this all
# the time because this fetches a ref into the source project, which
# isn't needed for reading.
def index
@index ||= @repository.rugged.merge_commits(@our_commit, @their_commit)
end
def write_resolved_file_to_index(file, params)
if params[:sections]
resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
new_file << "\n" if file.our_blob.data.ends_with?("\n")
elsif params[:content]
new_file = file.resolve_content(params[:content])
end
our_path = file.our_path
index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode)
index.conflict_remove(our_path)
end
end
end
end
end
......@@ -5,6 +5,8 @@ require 'open3'
module Gitlab
module Git
module Popen
FAST_GIT_PROCESS_TIMEOUT = 15.seconds
def popen(cmd, path, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
......@@ -27,6 +29,67 @@ module Gitlab
[@cmd_output, @cmd_status]
end
def popen_with_timeout(cmd, timeout, path, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
path ||= Dir.pwd
vars['PWD'] = path
unless File.directory?(path)
FileUtils.mkdir_p(path)
end
rout, wout = IO.pipe
rerr, werr = IO.pipe
pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true)
begin
status = process_wait_with_timeout(pid, timeout)
# close write ends so we could read them
wout.close
werr.close
cmd_output = rout.readlines.join
cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output
[cmd_output, status.exitstatus]
rescue Timeout::Error => e
kill_process_group_for_pid(pid)
raise e
ensure
wout.close unless wout.closed?
werr.close unless werr.closed?
rout.close
rerr.close
end
end
def process_wait_with_timeout(pid, timeout)
deadline = timeout.seconds.from_now
wait_time = 0.01
while deadline > Time.now
sleep(wait_time)
_, status = Process.wait2(pid, Process::WNOHANG)
return status unless status.nil?
end
raise Timeout::Error, "Timeout waiting for process ##{pid}"
end
def kill_process_group_for_pid(pid)
Process.kill("KILL", -pid)
Process.wait(pid)
rescue Errno::ESRCH
end
end
end
end
......@@ -1061,6 +1061,13 @@ module Gitlab
end
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git_with_timeout(args, timeout, env: {})
circuit_breaker.perform do
popen_with_timeout([Gitlab.config.git.bin_path, *args], timeout, path, env)
end
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def commit(ref = 'HEAD')
Gitlab::Git::Commit.find(self, ref)
......@@ -1092,6 +1099,24 @@ module Gitlab
popen(args, @path).last.zero?
end
def blob_at(sha, path)
Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha)
end
def commit_index(user, branch_name, index, options)
committer = user_to_committer(user)
OperationService.new(user, self).with_branch(branch_name) do
commit_params = options.merge(
tree: index.write_tree(rugged),
author: committer,
committer: committer
)
create_commit(commit_params)
end
end
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository)
end
......
......@@ -38,7 +38,7 @@ module Gitlab
end
def generate_line_code(line)
Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end
def on_diff?
......
module Gitlab
module QuickActions
# This class takes spend command argument
# and separates date and time from spend command arguments if it present
# example:
# spend_command_time_and_date = "15m 2017-01-02"
# SpendTimeAndDateSeparator.new(spend_command_time_and_date).execute
# => [900, Mon, 02 Jan 2017]
# if date doesn't present return time with current date
# in other cases return nil
class SpendTimeAndDateSeparator
DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/
def initialize(spend_command_arg)
@spend_arg = spend_command_arg
end
def execute
return if @spend_arg.blank?
return [get_time, DateTime.now.to_date] unless date_present?
return unless valid_date?
[get_time, get_date]
end
private
def get_time
raw_time = @spend_arg.gsub(DATE_REGEX, '')
Gitlab::TimeTrackingFormatter.parse(raw_time)
end
def get_date
string_date = @spend_arg.match(DATE_REGEX)[0]
Date.parse(string_date)
end
def date_present?
DATE_REGEX =~ @spend_arg
end
def valid_date?
string_date = @spend_arg.match(DATE_REGEX)[0]
date = Date.parse(string_date) rescue nil
date_past_or_today?(date)
end
def date_past_or_today?(date)
date&.past? || date&.today?
end
end
end
end
#!/bin/bash
psql -h postgres -U postgres gitlabhq_geo_test <<EOF
CREATE EXTENSION postgres_fdw;
CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'localhost', dbname 'gitlabhq_test');
CREATE USER MAPPING FOR current_user SERVER gitlab_secondary OPTIONS (user 'postgres', password '');
CREATE SCHEMA gitlab_secondary;
IMPORT FOREIGN SCHEMA public FROM SERVER gitlab_secondary INTO gitlab_secondary;
GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO current_user;
EOF
......@@ -17,8 +17,8 @@ describe Projects::MergeRequests::ConflictsController do
describe 'GET show' do
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser)
.to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
get :show,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
......@@ -109,8 +109,8 @@ describe Projects::MergeRequests::ConflictsController do
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser)
.to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
conflict_for_path('files/ruby/regex.rb')
end
......
require 'spec_helper'
describe 'label issues', :js do
include BoardHelpers
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let(:board) { create(:board, group: group) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:issue) { create(:labeled_issue, project: project, labels: [development]) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
before do
stub_licensed_features(group_issue_boards: true)
group.add_master(user)
sign_in(user)
visit group_boards_path(group)
wait_for_requests
end
it 'adds a new label from sidebar' do
card = find('.board:nth-child(2)').first('.card')
click_card(card)
page.within '.right-sidebar .labels' do
click_link 'Edit'
click_link 'Create new label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
wait_for_requests
end
page.within '.labels' do
expect(page).to have_link 'test label'
end
end
end
require 'rails_helper'
describe 'Issue Boards', :js do
include BoardHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
......@@ -137,19 +139,4 @@ describe 'Issue Boards', :js do
end
end
end
def click_card(card)
page.within(card) do
first('.card-number').click
end
wait_for_sidebar
end
def wait_for_sidebar
# loop until the CSS transition is complete
Timeout.timeout(0.5) do
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
end
end
end
require 'rails_helper'
describe 'Issue Boards', :js do
include BoardHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
......@@ -310,6 +312,21 @@ describe 'Issue Boards', :js do
expect(card).to have_selector('.label', count: 1)
expect(card).not_to have_content(stretch.title)
end
it 'creates new label' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
click_link 'Create new label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
wait_for_requests
expect(page).to have_link 'test label'
end
end
end
context 'subscription' do
......@@ -323,19 +340,4 @@ describe 'Issue Boards', :js do
end
end
end
def click_card(card)
page.within(card) do
first('.card-number').click
end
wait_for_sidebar
end
def wait_for_sidebar
# loop until the CSS transition is complete
Timeout.timeout(0.5) do
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
end
end
end
/* global ShortcutsIssuable */
import '~/copy_as_gfm';
import '~/shortcuts_issuable';
import ShortcutsIssuable from '~/shortcuts_issuable';
describe('ShortcutsIssuable', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
......
/* global Shortcuts */
import Shortcuts from '~/shortcuts';
describe('Shortcuts', () => {
const fixtureName = 'merge_requests/diff_comment.html.raw';
const createEvent = (type, target) => $.Event(type, {
......@@ -8,19 +9,17 @@ describe('Shortcuts', () => {
preloadFixtures(fixtureName);
describe('toggleMarkdownPreview', () => {
let sc;
beforeEach(() => {
loadFixtures(fixtureName);
spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus');
spyOnEvent('.edit-note .js-md-preview-button', 'focus');
sc = new Shortcuts();
new Shortcuts(); // eslint-disable-line no-new
});
it('focuses preview button in form', () => {
sc.toggleMarkdownPreview(
Shortcuts.toggleMarkdownPreview(
createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'),
));
......@@ -31,7 +30,7 @@ describe('Shortcuts', () => {
document.querySelector('.js-note-edit').click();
setTimeout(() => {
sc.toggleMarkdownPreview(
Shortcuts.toggleMarkdownPreview(
createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'),
));
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Conflict::FileCollection do
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
let(:file_collection) { described_class.read_only(merge_request) }
let(:file_collection) { described_class.new(merge_request) }
describe '#files' do
it 'returns an array of Conflict::Files' do
......
......@@ -8,9 +8,10 @@ describe Gitlab::Conflict::File do
let(:our_commit) { rugged.branches['conflict-resolvable'].target }
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
let(:index) { rugged.merge_commits(our_commit, their_commit) }
let(:conflict) { index.conflicts.last }
let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
let(:conflict_file) { described_class.new(merge_file_result, conflict, merge_request: merge_request) }
let(:rugged_conflict) { index.conflicts.last }
let(:raw_conflict_content) { index.merge_file('files/ruby/regex.rb')[:data] }
let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) }
let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
describe '#resolve_lines' do
let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
......@@ -48,18 +49,18 @@ describe Gitlab::Conflict::File do
end
end
it 'raises MissingResolution when passed a hash without resolutions for all sections' do
it 'raises ResolutionError when passed a hash without resolutions for all sections' do
empty_hash = section_keys.map { |key| [key, nil] }.to_h
invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
expect { conflict_file.resolve_lines({}) }
.to raise_error(Gitlab::Conflict::File::MissingResolution)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
expect { conflict_file.resolve_lines(empty_hash) }
.to raise_error(Gitlab::Conflict::File::MissingResolution)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
expect { conflict_file.resolve_lines(invalid_hash) }
.to raise_error(Gitlab::Conflict::File::MissingResolution)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
......@@ -144,7 +145,7 @@ describe Gitlab::Conflict::File do
end
context 'with an example file' do
let(:file) do
let(:raw_conflict_content) do
<<FILE
# Ensure there is no match line header here
def username_regexp
......@@ -220,7 +221,6 @@ end
FILE
end
let(:conflict_file) { described_class.new({ data: file }, conflict, merge_request: merge_request) }
let(:sections) { conflict_file.sections }
it 'sets the correct match line headers' do
......
......@@ -40,7 +40,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -108,7 +108,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 15)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -149,7 +149,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -189,7 +189,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, 13, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -233,7 +233,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 5)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -274,7 +274,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -314,7 +314,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, 4, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -357,7 +357,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -399,7 +399,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......@@ -447,7 +447,7 @@ describe Gitlab::Diff::Position do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line)
line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line)
expect(subject.line_code(project.repository)).to eq(line_code)
end
......
require 'spec_helper'
describe Gitlab::Geo do
describe Gitlab::Geo, :geo do
include ::EE::GeoHelpers
set(:primary_node) { create(:geo_node, :primary) }
......@@ -18,6 +18,26 @@ describe Gitlab::Geo do
end
end
describe 'fdw?' do
let(:fdw_check) { "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'gitlab_secondary' AND table_type = 'FOREIGN TABLE'" }
before do
allow(::Geo::BaseRegistry.connection).to receive(:execute).with(anything).and_call_original
end
it 'returns true when PostgreSQL FDW is enabled' do
expect(::Geo::BaseRegistry.connection).to receive(:execute).with(fdw_check).and_return([{ 'count' => 1 }])
expect(described_class.fdw?).to be_truthy
end
it 'returns false when PostgreSQL FDW is not enabled' do
expect(::Geo::BaseRegistry.connection).to receive(:execute).with(fdw_check).and_return([{ 'count' => 0 }])
expect(described_class.fdw?).to be_falsey
end
end
describe 'primary?' do
context 'when current node is a primary node' do
it 'returns true' do
......
require 'spec_helper'
describe Gitlab::Conflict::Parser do
let(:parser) { described_class.new }
describe '#parse' do
describe Gitlab::Git::Conflict::Parser do
describe '.parse' do
def parse_text(text)
parser.parse(text, our_path: 'README.md', their_path: 'README.md')
described_class.parse(text, our_path: 'README.md', their_path: 'README.md')
end
context 'when the file has valid conflicts' do
......@@ -87,33 +85,37 @@ CONFLICT
end
let(:lines) do
parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
described_class.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
end
let(:old_line_numbers) do
lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] }
end
let(:new_line_numbers) do
lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] }
end
let(:line_indexes) { lines.map { |line| line[:line_obj_index] } }
it 'sets our lines as new lines' do
expect(lines[8..13]).to all(have_attributes(type: 'new'))
expect(lines[26..27]).to all(have_attributes(type: 'new'))
expect(lines[56..57]).to all(have_attributes(type: 'new'))
expect(lines[8..13]).to all(include(type: 'new'))
expect(lines[26..27]).to all(include(type: 'new'))
expect(lines[56..57]).to all(include(type: 'new'))
end
it 'sets their lines as old lines' do
expect(lines[14..19]).to all(have_attributes(type: 'old'))
expect(lines[28..29]).to all(have_attributes(type: 'old'))
expect(lines[58..59]).to all(have_attributes(type: 'old'))
expect(lines[14..19]).to all(include(type: 'old'))
expect(lines[28..29]).to all(include(type: 'old'))
expect(lines[58..59]).to all(include(type: 'old'))
end
it 'sets non-conflicted lines as both' do
expect(lines[0..7]).to all(have_attributes(type: nil))
expect(lines[20..25]).to all(have_attributes(type: nil))
expect(lines[30..55]).to all(have_attributes(type: nil))
expect(lines[60..62]).to all(have_attributes(type: nil))
expect(lines[0..7]).to all(include(type: nil))
expect(lines[20..25]).to all(include(type: nil))
expect(lines[30..55]).to all(include(type: nil))
expect(lines[60..62]).to all(include(type: nil))
end
it 'sets consecutive line numbers for index, old_pos, and new_pos' do
old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
expect(lines.map(&:index)).to eq(0.upto(62).to_a)
it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do
expect(line_indexes).to eq(0.upto(62).to_a)
expect(old_line_numbers).to eq(1.upto(53).to_a)
expect(new_line_numbers).to eq(1.upto(53).to_a)
end
......@@ -123,12 +125,12 @@ CONFLICT
context 'when there is a non-start delimiter first' do
it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
expect { parse_text('=======') }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'raises UnexpectedDelimiter when there is an end delimiter first' do
expect { parse_text('>>>>>>> README.md') }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'does not raise when there is an end delimiter for a different path first' do
......@@ -143,12 +145,12 @@ CONFLICT
it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
expect { parse_text(start_text + start_text + end_text) }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'does not raise when it is followed by a start delimiter for a different path' do
......@@ -163,12 +165,12 @@ CONFLICT
it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
expect { parse_text(start_text + '=======' + end_text) }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
expect { parse_text(start_text + start_text + end_text) }
.to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter)
end
it 'does not raise when it is followed by a start delimiter for another path' do
......@@ -181,25 +183,25 @@ CONFLICT
start_text = "<<<<<<< README.md\n=======\n"
expect { parse_text(start_text) }
.to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter)
expect { parse_text(start_text + '>>>>>>> some-other-path.md') }
.to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
.to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter)
end
end
context 'other file types' do
it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
expect { parse_text('') }
.to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
.to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
expect { parse_text(nil) }
.to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
.to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
end
it 'raises UnmergeableFile when the file is over 200 KB' do
expect { parse_text('a' * 204801) }
.to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
.to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile)
end
# All text from Rugged has an encoding of ASCII_8BIT, so force that in
......@@ -214,7 +216,7 @@ CONFLICT
context 'when the file contains non-UTF-8 characters' do
it 'raises UnsupportedEncoding' do
expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
.to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
.to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding)
end
end
end
......
require 'spec_helper'
describe 'Gitlab::Git::Popen' do
let(:path) { Rails.root.join('tmp').to_s }
let(:klass) do
Class.new(Object) do
include Gitlab::Git::Popen
end
end
context 'popen' do
context 'zero status' do
let(:result) { klass.new.popen(%w(ls), path) }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to be_zero }
it { expect(output).to include('tests') }
end
context 'non-zero status' do
let(:result) { klass.new.popen(%w(cat NOTHING), path) }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to eq(1) }
it { expect(output).to include('No such file or directory') }
end
context 'unsafe string command' do
it 'raises an error when it gets called with a string argument' do
expect { klass.new.popen('ls', path) }.to raise_error(RuntimeError)
end
end
context 'with custom options' do
let(:vars) { { 'foobar' => 123, 'PWD' => path } }
let(:options) { { chdir: path } }
it 'calls popen3 with the provided environment variables' do
expect(Open3).to receive(:popen3).with(vars, 'ls', options)
klass.new.popen(%w(ls), path, { 'foobar' => 123 })
end
end
context 'use stdin' do
let(:result) { klass.new.popen(%w[cat], path) { |stdin| stdin.write 'hello' } }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to be_zero }
it { expect(output).to eq('hello') }
end
end
context 'popen_with_timeout' do
let(:timeout) { 1.second }
context 'no timeout' do
context 'zero status' do
let(:result) { klass.new.popen_with_timeout(%w(ls), timeout, path) }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to be_zero }
it { expect(output).to include('tests') }
end
context 'non-zero status' do
let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) }
let(:output) { result.first }
let(:status) { result.last }
it { expect(status).to eq(1) }
it { expect(output).to include('No such file or directory') }
end
context 'unsafe string command' do
it 'raises an error when it gets called with a string argument' do
expect { klass.new.popen_with_timeout('ls', timeout, path) }.to raise_error(RuntimeError)
end
end
end
context 'timeout' do
context 'timeout' do
it "raises a Timeout::Error" do
expect { klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) }.to raise_error(Timeout::Error)
end
it "handles processes that do not shutdown correctly" do
expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
end
end
context 'timeout period' do
let(:time_taken) do
begin
start = Time.now
klass.new.popen_with_timeout(%w(sleep 1000), timeout, path)
rescue
Time.now - start
end
end
it { expect(time_taken).to be >= timeout }
end
context 'clean up' do
let(:instance) { klass.new }
it 'kills the child process' do
expect(instance).to receive(:kill_process_group_for_pid).and_wrap_original do |m, *args|
# is the PID, and it should not be running at this point
m.call(*args)
pid = args.first
begin
Process.getpgid(pid)
raise "The child process should have been killed"
rescue Errno::ESRCH
end
end
expect { instance.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
end
end
end
end
end
......@@ -516,6 +516,7 @@ Timelog:
- merge_request_id
- issue_id
- user_id
- spent_at
- created_at
- updated_at
ProjectAutoDevops:
......
require 'spec_helper'
describe Gitlab::QuickActions::SpendTimeAndDateSeparator do
subject { described_class }
shared_examples 'arg line with invalid parameters' do
it 'return nil' do
expect(subject.new(invalid_arg).execute).to eq(nil)
end
end
shared_examples 'arg line with valid parameters' do
it 'return time and date array' do
expect(subject.new(valid_arg).execute).to eq(expected_response)
end
end
describe '#execute' do
context 'invalid paramenter in arg line' do
context 'empty arg line' do
it_behaves_like 'arg line with invalid parameters' do
let(:invalid_arg) { '' }
end
end
context 'future date in arg line' do
it_behaves_like 'arg line with invalid parameters' do
let(:invalid_arg) { '10m 6023-02-02' }
end
end
context 'unparseable date(invalid mixes of delimiters)' do
it_behaves_like 'arg line with invalid parameters' do
let(:invalid_arg) { '10m 2017.02-02' }
end
end
context 'trash in arg line' do
let(:invalid_arg) { 'dfjkghdskjfghdjskfgdfg' }
it 'return nil as time value' do
time_date_response = subject.new(invalid_arg).execute
expect(time_date_response).to be_an_instance_of(Array)
expect(time_date_response.first).to eq(nil)
end
end
end
context 'only time present in arg line' do
it_behaves_like 'arg line with valid parameters' do
let(:valid_arg) { '2m 3m 5m 1h' }
let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) }
let(:date) { DateTime.now.to_date }
let(:expected_response) { [time, date] }
end
end
context 'simple time with date in arg line' do
it_behaves_like 'arg line with valid parameters' do
let(:raw_time) { '10m' }
let(:raw_date) { '2016-02-02' }
let(:valid_arg) { "#{raw_time} #{raw_date}" }
let(:date) { Date.parse(raw_date) }
let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) }
let(:expected_response) { [time, date] }
end
end
context 'composite time with date in arg line' do
it_behaves_like 'arg line with valid parameters' do
let(:raw_time) { '2m 10m 1h 3d' }
let(:raw_date) { '2016/02/02' }
let(:valid_arg) { "#{raw_time} #{raw_date}" }
let(:date) { Date.parse(raw_date) }
let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) }
let(:expected_response) { [time, date] }
end
end
end
end
......@@ -105,7 +105,7 @@ describe DiffNote do
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15)
line_code = Gitlab::Git.diff_line_code(position.file_path, position.formatter.new_line, 15)
expect(subject.line_code).to eq(line_code)
end
......
......@@ -35,7 +35,7 @@ describe MergeRequests::Conflicts::ListService do
it 'returns a falsey value when the MR has a missing ref after a force push' do
merge_request = create_merge_request('conflict-resolvable')
service = conflicts_service(merge_request)
allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError)
expect(service.can_be_resolved_in_ui?).to be_falsey
end
......
......@@ -107,25 +107,27 @@ describe MergeRequests::Conflicts::ResolveService do
branch_name: 'conflict-start')
end
def resolve_conflicts
subject do
described_class.new(merge_request_from_fork).execute(user, params)
end
it 'gets conflicts from the source project' do
# REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't
# used in this case, but since the refactor, for simplification,
# we always use that repository for read only operations.
expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
expect(project.repository.rugged).not_to receive(:merge_commits)
resolve_conflicts
subject
end
it 'creates a commit with the message' do
resolve_conflicts
subject
expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
end
it 'creates a commit with the correct parents' do
resolve_conflicts
subject
expect(merge_request_from_fork.source_branch_head.parents.map(&:id))
.to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head])
......@@ -200,14 +202,19 @@ describe MergeRequests::Conflicts::ResolveService do
}
end
it 'raises a MissingResolution error' do
it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
.to raise_error(Gitlab::Conflict::File::MissingResolution)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
context 'when the content of a file is unchanged' do
let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) }
let(:resolver) do
MergeRequests::Conflicts::ListService.new(merge_request).conflicts.resolver
end
let(:regex_conflict) do
resolver.conflict_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb')
end
let(:invalid_params) do
{
......@@ -219,16 +226,16 @@ describe MergeRequests::Conflicts::ResolveService do
}, {
old_path: 'files/ruby/regex.rb',
new_path: 'files/ruby/regex.rb',
content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content
content: regex_conflict.content
}
],
commit_message: 'This is a commit message!'
}
end
it 'raises a MissingResolution error' do
it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
.to raise_error(Gitlab::Conflict::File::MissingResolution)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
......@@ -246,9 +253,9 @@ describe MergeRequests::Conflicts::ResolveService do
}
end
it 'raises a MissingFiles error' do
it 'raises a ResolutionError error' do
expect { service.execute(user, invalid_params) }
.to raise_error(described_class::MissingFiles)
.to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError)
end
end
end
......
......@@ -209,7 +209,11 @@ describe QuickActions::InterpretService do
it 'populates spend_time: 3600 if content contains /spend 1h' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: { duration: 3600, user: developer })
expect(updates).to eq(spend_time: {
duration: 3600,
user: developer,
spent_at: DateTime.now.to_date
})
end
end
......@@ -217,7 +221,39 @@ describe QuickActions::InterpretService do
it 'populates spend_time: -1800 if content contains /spend -30m' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: { duration: -1800, user: developer })
expect(updates).to eq(spend_time: {
duration: -1800,
user: developer,
spent_at: DateTime.now.to_date
})
end
end
shared_examples 'spend command with valid date' do
it 'populates spend time: 1800 with date in date type format' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(spend_time: {
duration: 1800,
user: developer,
spent_at: Date.parse(date)
})
end
end
shared_examples 'spend command with invalid date' do
it 'will not create any note and timelog' do
_, updates = service.execute(content, issuable)
expect(updates).to eq({})
end
end
shared_examples 'spend command with future date' do
it 'will not create any note and timelog' do
_, updates = service.execute(content, issuable)
expect(updates).to eq({})
end
end
......@@ -711,6 +747,22 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
it_behaves_like 'spend command with valid date' do
let(:date) { '2016-02-02' }
let(:content) { "/spend 30m #{date}" }
let(:issuable) { issue }
end
it_behaves_like 'spend command with invalid date' do
let(:content) { '/spend 30m 17-99-99' }
let(:issuable) { issue }
end
it_behaves_like 'spend command with future date' do
let(:content) { '/spend 30m 6017-10-10' }
let(:issuable) { issue }
end
it_behaves_like 'empty command' do
let(:content) { '/spend' }
let(:issuable) { issue }
......
......@@ -181,6 +181,10 @@ RSpec.configure do |config|
example.run if Group.supports_nested_groups?
end
config.around(:each, :geo) do |example|
example.run if Gitlab::Database.postgresql?
end
config.around(:each, :postgresql) do |example|
example.run if Gitlab::Database.postgresql?
end
......
module BoardHelpers
def click_card(card)
within card do
first('.card-number').click
end
wait_for_sidebar
end
def wait_for_sidebar
# loop until the CSS transition is complete
Timeout.timeout(0.5) do
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
end
end
end
require 'spec_helper'
describe Geo::FileDownloadDispatchWorker, :postgresql do
describe Geo::FileDownloadDispatchWorker, :geo do
include ::EE::GeoHelpers
set(:primary) { create(:geo_node, :primary, host: 'primary-geo-node') }
......
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