Commit 90da0d4f authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'live-trace-v2' into live-trace-v2-efficient-destroy-all

parents b2d84185 30464bce
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.7.3 (2018-05-02)
### Fixed (8 changes)
- Fixed wrong avatar URL when the avatar is on object storage. !18092
- Fix errors on pushing to an empty repository. !18462
- Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication. !18543
- Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite. !18580
- Fix redirection error for applications using OpenID. !18599
- Fix commit trailer rendering when Gravatar is disabled.
- Fix file_store for artifacts and lfs when saving.
- Fix users not seeing labels from private groups when being a member of a child project.
## 10.7.2 (2018-04-25) ## 10.7.2 (2018-04-25)
### Security (2 changes) ### Security (2 changes)
......
...@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1' ...@@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# User agent parsing
gem 'device_detector'
# Cache # Cache
gem 'redis-rails', '~> 5.0.2' gem 'redis-rails', '~> 5.0.2'
......
...@@ -161,6 +161,7 @@ GEM ...@@ -161,6 +161,7 @@ GEM
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
devise (4.2.0) devise (4.2.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -1026,6 +1027,7 @@ DEPENDENCIES ...@@ -1026,6 +1027,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
device_detector
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
......
...@@ -304,12 +304,12 @@ GEM ...@@ -304,12 +304,12 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-gollum-lib (4.2.7.1) gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2) gemojione (~> 3.2)
github-markup (~> 1.6) github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0) nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1) rouge (~> 3.1)
sanitize (~> 2.1) sanitize (~> 2.1)
stringex (~> 2.6) stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4) gitlab-gollum-rugged_adapter (0.4.4)
...@@ -602,8 +602,6 @@ GEM ...@@ -602,8 +602,6 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
peek peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0) peek-pg (1.3.0)
concurrent-ruby concurrent-ruby
concurrent-ruby-ext concurrent-ruby-ext
...@@ -752,7 +750,7 @@ GEM ...@@ -752,7 +750,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.4) rinku (2.0.4)
rotp (2.1.2) rotp (2.1.2)
rouge (2.2.1) rouge (3.1.1)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -1134,7 +1132,6 @@ DEPENDENCIES ...@@ -1134,7 +1132,6 @@ DEPENDENCIES
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0) peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
...@@ -1166,7 +1163,7 @@ DEPENDENCIES ...@@ -1166,7 +1163,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
......
...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() { ...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
const { const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
name,
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call( const isEmojiUnicode =
this.childNodes, this.childNodes &&
childNode => childNode.nodeType === 3, Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import $ from 'jquery';
import { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
$('.js-compare-dropdown').each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data('fieldName'),
filterable: true,
id: function(obj, $el) {
return $el.data('id');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(e, el) {
if ($dropdown.is('.js-target-branch')) {
return _this.getTargetHtml();
} else if ($dropdown.is('.js-source-branch')) {
return _this.getSourceHtml();
} else if ($dropdown.is('.js-target-project')) {
return _this.getTargetProject();
}
}
});
};
})(this));
this.initialState();
}
initialState() {
this.getSourceHtml();
this.getTargetHtml();
}
getTargetProject() {
$('.mr_target_commit').empty();
return axios.get(this.opts.targetProjectUrl, {
params: {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
},
}).then(({ data }) => {
$('.js-target-branch-dropdown .dropdown-content').html(data);
});
}
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
}
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
}
static sendAjax(url, loading, target, params) {
const $target = $(target);
loading.show();
$target.empty();
return axios.get(url, {
params,
}).then(({ data }) => {
loading.hide();
$target.html(data);
const className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
});
}
}
...@@ -4,8 +4,9 @@ import $ from 'jquery'; ...@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale'; import { __ } from './locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete() { export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() { $('.js-compare-dropdown').each(function() {
var $dropdown, selected; var $dropdown, selected;
$dropdown = $(this); $dropdown = $(this);
...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() { ...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({ $dropdown.glDropdown({
data: function(term, callback) { data: function(term, callback) {
axios.get($dropdown.data('refsUrl'), { const params = {
params: { ref: $dropdown.data('ref'),
ref: $dropdown.data('ref'), search: term,
search: term, };
},
}).then(({ data }) => { if (limitTo) {
callback(data); params.find = limitTo;
}).catch(() => flash(__('Error fetching refs'))); }
axios
.get($dropdown.data('refsUrl'), {
params,
})
.then(({ data }) => {
if (limitTo) {
callback(data[capitalizeFirstCharacter(limitTo)] || []);
} else {
callback(data);
}
})
.catch(() => flash(__('Error fetching refs')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() { ...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) { renderRow: function(ref) {
var link; var link;
if (ref.header != null) { if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header); return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else { } else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', escape(ref));
return $('<li />').append(link); return $('<li />').append(link);
} }
}, },
...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() { ...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
}, },
toggleLabel: function(obj, $el) { toggleLabel: function(obj, $el) {
return $el.text().trim(); return $el.text().trim();
} },
clicked: () => clickHandler($dropdown),
}); });
$filterInput.on('keyup', (e) => { $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which; const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return; if (keyCode !== 13) return;
const text = $filterInput.val(); const text = $filterInput.val();
...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() { ...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open'); $dropdownContainer.removeClass('open');
}); });
$dropdownContainer.on('click', '.dropdown-content a', (e) => { $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) { if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle'); $dropdown.tooltip('fixTitle');
......
...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() { ...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [], symbols: [],
flags: [], flags: [],
}; };
Object.keys(emojiMap).forEach((name) => { Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name]; const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) { if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name); emojiCategoryMap[emoji.category].push(name);
...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) { ...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass); classList.push(fallbackSpriteClass);
} }
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: '';
let contents = emojiInfo.moji; let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) { if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc); contents = emojiImageTag(name, fallbackImageSrc);
......
...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = { ...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) { function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA // `4 *` because RGBA
const indexOffset = 4 * pixelOffset; const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] || const hasColor =
imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] || imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2]; imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3]; const isVisible = imageDataArray[indexOffset + 3];
...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche ...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16; const fontSize = 16;
function generateUnicodeSupportMap(testMap) { function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap); const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length; .length;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize); canvas.width = 2 * fontSize;
canvas.height = (numTestEntries * fontSize); canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => { [].concat(testEntry).forEach(emojiUnicode => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1; writeIndex += 1;
}); });
}); });
...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) { ...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas // Read from the canvas
const resultMap = {}; const resultMap = {};
let readIndex = 0; let readIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to // This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries // keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters // Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData( const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
0, .data;
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false; let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize; const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row // Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true; isValidEmoji = true;
// Check to see that nothing is rendered next to the first character // Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false; isValidEmoji = false;
break; break;
...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() { ...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) { if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION); window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); window.localStorage.setItem(
'gl-emoji-unicode-support-map',
JSON.stringify(unicodeSupportMap),
);
} }
} }
......
import initCompareAutocomplete from '~/compare_autocomplete'; import initCompareAutocomplete from '~/compare_autocomplete';
document.addEventListener('DOMContentLoaded', initCompareAutocomplete); document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
import $ from 'jquery';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import initCompareAutocomplete from '~/compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
$loadingIndicator.show();
$commitList.empty();
return axios
.get(url, {
params,
})
.then(({ data }) => {
$loadingIndicator.hide();
$commitList.html(data);
localTimeAgo($('.js-timeago', $commitList));
});
};
export default mrNewCompareNode => {
const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
initTargetProjectDropdown();
const updateSourceBranchCommitList = () =>
updateCommitList(
sourceBranchUrl,
$(mrNewCompareNode).find('.js-source-loading'),
$(mrNewCompareNode).find('.mr_source_commit'),
{
ref: $(mrNewCompareNode)
.find("input[name='merge_request[source_branch]']")
.val(),
},
);
const updateTargetBranchCommitList = () =>
updateCommitList(
targetBranchUrl,
$(mrNewCompareNode).find('.js-target-loading'),
$(mrNewCompareNode).find('.mr_target_commit'),
{
target_project_id: $(mrNewCompareNode)
.find("input[name='merge_request[target_project_id]']")
.val(),
ref: $(mrNewCompareNode)
.find("input[name='merge_request[target_branch]']")
.val(),
},
);
initCompareAutocomplete('branches', $dropdown => {
if ($dropdown.is('.js-target-branch')) {
updateTargetBranchCommitList();
} else if ($dropdown.is('.js-source-branch')) {
updateSourceBranchCommitList();
}
});
updateSourceBranchCommitList();
updateTargetBranchCommitList();
};
import Compare from '~/compare';
import MergeRequest from '~/merge_request'; import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) { if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new initCompare(mrNewCompareNode);
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else { } else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new // eslint-disable-next-line no-new
new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction, action: mrNewSubmitNode.dataset.mrSubmitAction,
}); });
initPipelines(); initPipelines();
......
import $ from 'jquery';
export default () => {
const $targetProjectDropdown = $('.js-target-project');
$targetProjectDropdown.glDropdown({
selectable: true,
fieldName: $targetProjectDropdown.data('fieldName'),
filterable: true,
id(obj, $el) {
return $el.data('id');
},
toggleLabel(obj, $el) {
return $el.text().trim();
},
clicked({ $el }) {
$('.mr_target_commit').empty();
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
$targetBranchDropdown.data('glDropdown').clearMenu();
},
});
};
export default {
name: 'time-tracking-no-tracking-pane',
template: `
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
`,
};
<script>
export default {
name: 'TimeTrackingNoTrackingPane',
};
</script>
<template>
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
</template>
<script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator'; ...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
components: {
IssuableTimeTracker,
},
data() { data() {
return { return {
mediator: new Mediator(), mediator: new Mediator(),
store: new Store(), store: new Store(),
}; };
}, },
components: { mounted() {
IssuableTimeTracker, this.listenForQuickActions();
}, },
methods: { methods: {
listenForQuickActions() { listenForQuickActions() {
...@@ -41,18 +45,17 @@ export default { ...@@ -41,18 +45,17 @@ export default {
} }
}, },
}, },
mounted() {
this.listenForQuickActions();
},
template: `
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:rootPath="store.rootPath"
/>
</div>
`,
}; };
</script>
<template>
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:root-path="store.rootPath"
/>
</div>
</template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane'; import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane, TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
TimeTrackingHelpState, TimeTrackingHelpState,
}, },
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
......
<script>
import $ from 'jquery'; import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'MRWidgetWIP', name: 'WorkInProgress',
props: { components: {
mr: { type: Object, required: true }, statusIcon,
service: { type: Object, required: true },
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() { data() {
return { return {
isMakingRequest: false, isMakingRequest: false,
}; };
}, },
components: {
statusIcon,
},
methods: { methods: {
removeWIP() { removeWIP() {
this.isMakingRequest = true; this.isMakingRequest = true;
...@@ -36,32 +37,40 @@ export default { ...@@ -36,32 +37,40 @@ export default {
}); });
}, },
}, },
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Resolve WIP status
</button>
</div>
</div>
`,
}; };
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="Boolean(mr.removeWIPPath)"
/>
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Resolve WIP status
</button>
</div>
</div>
</template>
...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue ...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
ClosedState, ClosedState,
MergingState, MergingState,
RebaseState, RebaseState,
WipState, WorkInProgressState,
ArchivedState, ArchivedState,
ConflictsState, ConflictsState,
NothingToMergeState, NothingToMergeState,
...@@ -220,7 +220,7 @@ export default { ...@@ -220,7 +220,7 @@ export default {
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState, 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge, 'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState, 'mr-widget-wip': WorkInProgressState,
'mr-widget-archived': ArchivedState, 'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState, 'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState, 'mr-widget-nothing-to-merge': NothingToMergeState,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
@import "framework/variables"; @import 'framework/variables';
@import "framework/mixins"; @import 'framework/mixins';
@import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap'; @import 'framework/tw_bootstrap';
@import "framework/layout"; @import 'framework/layout';
@import "framework/animations"; @import 'framework/animations';
@import "framework/vue_transitions"; @import 'framework/vue_transitions';
@import "framework/avatar"; @import 'framework/avatar';
@import "framework/asciidoctor"; @import 'framework/asciidoctor';
@import "framework/banner"; @import 'framework/banner';
@import "framework/blocks"; @import 'framework/blocks';
@import "framework/buttons"; @import 'framework/buttons';
@import "framework/badges"; @import 'framework/badges';
@import "framework/calendar"; @import 'framework/calendar';
@import "framework/callout"; @import 'framework/callout';
@import "framework/common"; @import 'framework/common';
@import "framework/dropdowns"; @import 'framework/dropdowns';
@import "framework/files"; @import 'framework/files';
@import "framework/filters"; @import 'framework/filters';
@import "framework/flash"; @import 'framework/flash';
@import "framework/forms"; @import 'framework/forms';
@import "framework/gfm"; @import 'framework/gfm';
@import "framework/gitlab_theme"; @import 'framework/gitlab_theme';
@import "framework/header"; @import 'framework/header';
@import "framework/highlight"; @import 'framework/highlight';
@import "framework/issue_box"; @import 'framework/issue_box';
@import "framework/jquery"; @import 'framework/jquery';
@import "framework/lists"; @import 'framework/lists';
@import "framework/logo"; @import 'framework/logo';
@import "framework/markdown_area"; @import 'framework/markdown_area';
@import "framework/media_object"; @import 'framework/media_object';
@import "framework/mobile"; @import 'framework/mobile';
@import "framework/modal"; @import 'framework/modal';
@import "framework/pagination"; @import 'framework/pagination';
@import "framework/panels"; @import 'framework/panels';
@import "framework/popup"; @import 'framework/popup';
@import "framework/secondary_navigation_elements"; @import 'framework/secondary_navigation_elements';
@import "framework/selects"; @import 'framework/selects';
@import "framework/sidebar"; @import 'framework/sidebar';
@import "framework/contextual_sidebar"; @import 'framework/contextual_sidebar';
@import "framework/tables"; @import 'framework/tables';
@import "framework/notes"; @import 'framework/notes';
@import "framework/tabs"; @import 'framework/tabs';
@import "framework/timeline"; @import 'framework/timeline';
@import "framework/tooltips"; @import 'framework/tooltips';
@import "framework/toggle"; @import 'framework/toggle';
@import "framework/typography"; @import 'framework/typography';
@import "framework/zen"; @import 'framework/zen';
@import "framework/blank"; @import 'framework/blank';
@import "framework/wells"; @import 'framework/wells';
@import "framework/page_header"; @import 'framework/page_header';
@import "framework/awards"; @import 'framework/awards';
@import "framework/images"; @import 'framework/images';
@import "framework/broadcast_messages"; @import 'framework/broadcast_messages';
@import "framework/emojis"; @import 'framework/emojis';
@import "framework/emoji_sprites"; @import 'framework/icons';
@import "framework/icons"; @import 'framework/snippets';
@import "framework/snippets"; @import 'framework/memory_graph';
@import "framework/memory_graph"; @import 'framework/responsive_tables';
@import "framework/responsive_tables"; @import 'framework/stacked_progress_bar';
@import "framework/stacked_progress_bar"; @import 'framework/ci_variable_list';
@import "framework/ci_variable_list"; @import 'framework/feature_highlight';
@import "framework/feature_highlight";
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
} }
&.middle-block { &.middle-block {
margin-top: 0; margin-top: $gl-padding-24;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
} }
&.footer-block { &.footer-block {
margin-top: 0; margin-top: $gl-padding-24;
border-bottom: 0; border-bottom: 0;
margin-bottom: -$gl-padding; margin-bottom: -$gl-padding;
} }
......
...@@ -452,6 +452,7 @@ img.emoji { ...@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; } .prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; } .prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -39,35 +39,10 @@ ...@@ -39,35 +39,10 @@
svg { svg {
fill: currentColor; fill: currentColor;
&.s8 { $svg-sizes: 8 12 16 18 24 32 48 72;
@include svg-size(8px); @each $svg-size in $svg-sizes {
} &.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
&.s12 { }
@include svg-size(12px);
}
&.s16 {
@include svg-size(16px);
}
&.s18 {
@include svg-size(18px);
}
&.s24 {
@include svg-size(24px);
}
&.s32 {
@include svg-size(32px);
}
&.s48 {
@include svg-size(48px);
}
&.s72 {
@include svg-size(72px);
} }
} }
...@@ -107,6 +107,16 @@ ...@@ -107,6 +107,16 @@
padding-top: 10px; padding-top: 10px;
} }
.referenced-commands {
background: $blue-50;
padding: $gl-padding-8 $gl-padding;
border-radius: $border-radius-default;
p {
margin: 0;
}
}
.md-preview-holder { .md-preview-holder {
min-height: 167px; min-height: 167px;
padding: 10px 0; padding: 10px 0;
......
...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px; ...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
/* /*
* Padding * Padding
*/ */
$gl-padding-24: 24px;
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px; $gl-padding-8: 8px;
$gl-padding-4: 4px; $gl-padding-4: 4px;
......
.project-refs-form,
.project-refs-target-form {
display: inline-block;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.commit-message {
@include str-truncated(250px);
}
.editable-mode {
display: inline-block;
}
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 40px;
color: $almost-black;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
&.is-collapsed {
.ide-file-list {
max-width: 250px;
}
}
.file-status-icon {
width: 10px;
height: 10px;
}
}
.ide-file-list {
flex: 1;
.file {
cursor: pointer;
&.file-open {
background: $white-normal;
}
.ide-file-name {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
svg {
vertical-align: middle;
margin-right: 2px;
}
.loading-container {
margin-right: 4px;
display: inline-block;
}
}
.ide-file-changed-icon {
margin-left: auto;
}
.ide-new-btn {
display: none;
margin-bottom: -4px;
margin-right: -8px;
}
&:hover {
.ide-new-btn {
display: block;
}
}
&.folder {
svg {
fill: $gl-text-color-secondary;
}
}
}
a {
color: $gl-text-color;
}
th {
position: sticky;
top: 0;
}
}
.file-name,
.file-col-commit-message {
display: flex;
overflow: visible;
padding: 6px 12px;
}
.multi-file-loading-container {
margin-top: 10px;
padding: 10px;
.animation-container {
background: $gray-light;
div {
background: $gray-light;
}
}
}
.multi-file-table-col-commit-message {
white-space: nowrap;
width: 50%;
}
.multi-file-edit-pane {
display: flex;
flex-direction: column;
flex: 1;
border-left: 1px solid $white-dark;
overflow: hidden;
}
.multi-file-tabs {
display: flex;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
> ul {
display: flex;
overflow-x: auto;
}
li {
position: relative;
}
.dropdown {
display: flex;
margin-left: auto;
margin-bottom: 1px;
padding: 0 $grid-size;
border-left: 1px solid $white-dark;
background-color: $white-light;
&.shadow {
box-shadow: 0 0 10px $dropdown-shadow-color;
}
.btn {
margin-top: auto;
margin-bottom: auto;
}
}
}
.multi-file-tab {
@include str-truncated(150px);
padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
background-color: $gray-normal;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
cursor: pointer;
svg {
vertical-align: middle;
}
&.active {
background-color: $white-light;
border-bottom-color: $white-light;
}
}
.multi-file-tab-close {
position: absolute;
right: 8px;
top: 50%;
width: 16px;
height: 16px;
padding: 0;
background: none;
border: 0;
border-radius: $border-radius-default;
color: $theme-gray-900;
transform: translateY(-50%);
svg {
position: relative;
top: -1px;
}
&:hover {
background-color: $theme-gray-200;
}
&:focus {
background-color: $blue-500;
color: $white-light;
outline: 0;
svg {
fill: currentColor;
}
}
}
.multi-file-edit-pane-content {
flex: 1;
height: 0;
}
.blob-editor-container {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
justify-content: center;
.vertical-center {
min-height: auto;
}
.monaco-editor .lines-content .cigr {
display: none;
}
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
}
.diagonal-fill {
display: none !important;
}
.diffOverview {
background-color: $white-light;
border-left: 1px solid $white-dark;
cursor: ns-resize;
}
.diffViewport {
display: none;
}
.char-insert {
background-color: $line-added-dark;
}
.char-delete {
background-color: $line-removed-dark;
}
.line-numbers {
color: $black-transparent;
}
.view-overlays {
.line-insert {
background-color: $line-added;
}
.line-delete {
background-color: $line-removed;
}
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
.line-insert {
border-right: 1px solid $line-added-dark;
}
.line-delete {
border-right: 1px solid $line-removed-dark;
}
}
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
.cursors-layer {
display: none;
}
}
}
.multi-file-editor-holder {
height: 100%;
}
.multi-file-editor-btn-group {
padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
background: $white-light;
}
.ide-status-bar {
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
justify-content: space-between;
svg {
vertical-align: middle;
}
}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
height: 100%;
overflow: scroll;
.file-content.code {
display: flex;
i {
margin-left: -10px;
}
}
.line-numbers {
min-width: 50px;
}
.file-content,
.line-numbers,
.blob-content,
.code {
min-height: 100%;
}
}
.file-content.blob-no-preview {
a {
margin-left: auto;
margin-right: auto;
}
}
.multi-file-commit-panel {
display: flex;
position: relative;
flex-direction: column;
width: 340px;
padding: 0;
background-color: $gray-light;
padding-right: 3px;
.projects-sidebar {
display: flex;
flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
}
.multi-file-commit-panel-inner {
display: flex;
flex: 1;
flex-direction: column;
}
.multi-file-commit-panel-inner-scroll {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
&.is-collapsed {
width: 60px;
.multi-file-commit-list {
padding-top: $gl-padding;
overflow: hidden;
}
.multi-file-context-bar-icon {
align-items: center;
svg {
float: none;
margin: 0;
}
}
}
.branch-container {
border-left: 4px solid $indigo-700;
margin-bottom: $gl-bar-padding;
}
.branch-header {
background: $white-dark;
display: flex;
}
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
vertical-align: middle;
}
}
.branch-header-btns {
padding: $gl-vert-padding $gl-padding;
}
.left-collapse-btn {
display: none;
background: $gray-light;
text-align: left;
border-top: 1px solid $white-dark;
svg {
vertical-align: middle;
}
}
}
.multi-file-context-bar-icon {
padding: 10px;
svg {
margin-right: 10px;
float: left;
}
}
.multi-file-commit-panel-section {
display: flex;
flex-direction: column;
flex: 1;
}
.multi-file-commit-empty-state-container {
align-items: center;
justify-content: center;
}
.multi-file-commit-panel-header {
display: flex;
align-items: center;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
svg {
margin-left: auto;
margin-right: auto;
}
.multi-file-commit-panel-collapse-btn {
margin-right: auto;
margin-left: auto;
border-left: 0;
}
}
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
padding: 0 $gl-btn-padding;
svg {
margin-right: $gl-btn-padding;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
}
.multi-file-commit-list {
flex: 1;
overflow: auto;
padding: $gl-padding 0;
min-height: 60px;
}
.multi-file-commit-list-item {
display: flex;
padding: 0;
align-items: center;
.multi-file-discard-btn {
display: none;
margin-left: auto;
color: $gl-link-color;
padding: 0 2px;
&:focus,
&:hover {
text-decoration: underline;
}
}
&:hover {
background: $white-normal;
.multi-file-discard-btn {
display: block;
}
}
}
.multi-file-addition {
fill: $green-500;
}
.multi-file-modified {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
> svg {
margin-left: auto;
margin-right: auto;
}
.file-status-icon {
width: 10px;
height: 10px;
margin-left: 3px;
}
}
.multi-file-commit-list-path {
padding: $grid-size / 2;
padding-left: $gl-padding;
background: none;
border: 0;
text-align: left;
width: 100%;
min-width: 0;
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
&:hover,
&:focus {
outline: 0;
}
}
.multi-file-commit-list-file-path {
@include str-truncated(100%);
&:hover {
text-decoration: underline;
}
&:active {
text-decoration: none;
}
}
.multi-file-commit-form {
padding: $gl-padding;
border-top: 1px solid $white-dark;
.btn {
font-size: $gl-font-size;
}
}
.multi-file-commit-message.form-control {
height: 160px;
resize: none;
}
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
left: 0 !important;
&-modified {
background-color: $blue-500;
}
&-added {
background-color: $green-600;
}
&-removed {
height: 0 !important;
width: 0 !important;
bottom: -2px;
border-style: solid;
border-width: 5px;
border-color: transparent transparent transparent $red-500;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 1px;
background-color: rgba($red-500, 0.5);
}
}
}
.ide-loading {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
.ide-empty-state {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
.ide-new-btn {
.dropdown-toggle svg {
margin-top: -2px;
margin-bottom: 2px;
}
.dropdown-menu {
left: auto;
right: 0;
label {
font-weight: $gl-font-weight-normal;
padding: 5px 8px;
margin-bottom: 0;
}
}
}
.ide {
overflow: hidden;
&.nav-only {
.flash-container {
margin-top: $header-height;
margin-bottom: 0;
}
.alert-wrapper .flash-container .flash-alert:last-child,
.alert-wrapper .flash-container .flash-notice:last-child {
margin-bottom: 0;
}
.content-wrapper {
margin-top: $header-height;
padding-bottom: 0;
}
&.flash-shown {
.content-wrapper {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
}
.projects-sidebar {
.multi-file-commit-panel-inner-scroll {
flex: 1;
}
}
}
}
.with-performance-bar .ide.nav-only {
.flash-container {
margin-top: #{$header-height + $performance-bar-height};
}
.content-wrapper {
margin-top: #{$header-height + $performance-bar-height};
padding-bottom: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
&.flash-shown {
.content-wrapper {
margin-top: 0;
}
.ide-view {
height: calc(
100vh - #{$header-height + $performance-bar-height + $flash-height}
);
}
}
}
.dragHandle {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
background-color: $white-dark;
&.dragright {
right: 0;
}
&.dragleft {
left: 0;
}
}
.ide-commit-radios {
label {
font-weight: normal;
}
.help-block {
margin-top: 0;
line-height: 0;
}
}
.ide-commit-new-branch {
margin-left: 25px;
}
.ide-external-links {
p {
margin: 0;
}
}
.ide-sidebar-link {
padding: $gl-padding-8 $gl-padding;
background: $indigo-700;
color: $white-light;
text-decoration: none;
display: flex;
align-items: center;
&:focus,
&:hover {
color: $white-light;
text-decoration: underline;
background: $indigo-500;
}
&:active {
background: $indigo-800;
}
}
...@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base ...@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled? Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" } application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end end
......
...@@ -57,7 +57,7 @@ module IssuableCollections ...@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true))) redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end end
out_of_range out_of_range
......
...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController ...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group) def build_canonical_path(group)
params[:group_id] = group.to_param params[:group_id] = group.to_param
url_for(params) url_for(safe_params)
end end
end end
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
@sessions = ActiveSession.list(current_user)
end
def destroy
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: 302 }
format.js { head :ok }
end
end
end
...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param params[:project_id] = project.to_param
url_for(params) url_for(safe_params)
end end
def repository def repository
......
...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object) def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id) if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project object.lfs_objects_projects.create!(project: storage_project)
object.save!
end end
end end
end end
...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false render layout: false
end end
def update_branches
@target_project = selected_target_project
@target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false
end
private private
def build_merge_request def build_merge_request
......
module ActiveSessionsHelper
# Maps a device type as defined in `ActiveSession` to an svg icon name and
# outputs the icon html.
#
# see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
def active_session_device_type_icon(active_session)
icon_name =
case active_session.device_type
when 'smartphone', 'feature phone', 'phablet'
'mobile'
when 'tablet'
'tablet'
when 'tv', 'smart display', 'camera', 'portable media player', 'console'
'media'
when 'car browser'
'car'
else
'monitor-o'
end
sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
end
end
...@@ -442,7 +442,7 @@ module ProjectsHelper ...@@ -442,7 +442,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'), visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled, registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'), registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?, lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
} }
......
class ActiveSession
include ActiveModel::Model
attr_accessor :created_at, :updated_at,
:session_id, :ip_address,
:browser, :os, :device_name, :device_type
def current?(session)
return false if session_id.nil? || session.id.nil?
session_id == session.id
end
def human_device_type
device_type&.titleize
end
def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
session_id = request.session.id
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
active_user_session = new(
ip_address: request.ip,
browser: client.name,
os: client.os_name,
device_name: client.device_name,
device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp,
session_id: session_id
)
redis.pipelined do
redis.setex(
key_name(user.id, session_id),
Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session)
)
redis.sadd(
lookup_key_name(user.id),
session_id
)
end
end
end
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id).map do |entry|
# rubocop:disable Security/MarshalLoad
Marshal.load(entry)
# rubocop:enable Security/MarshalLoad
end
end
end
def self.destroy(user, session_id)
Gitlab::Redis::SharedState.with do |redis|
redis.srem(lookup_key_name(user.id), session_id)
deleted_keys = redis.del(key_name(user.id, session_id))
# only allow deleting the devise session if we could actually find a
# related active session. this prevents another user from deleting
# someone else's session.
if deleted_keys > 0
redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
end
end
end
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id)
end
end
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
def self.lookup_key_name(user_id)
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
def self.cleaned_up_lookup_entries(redis, user_id)
lookup_key = lookup_key_name(user_id)
session_ids = redis.smembers(lookup_key)
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
return [] if entry_keys.empty?
entries = redis.mget(entry_keys)
session_ids_and_entries = session_ids.zip(entries)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
redis.srem(lookup_key, session_id)
end
session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
end
end
...@@ -13,7 +13,7 @@ module Ci ...@@ -13,7 +13,7 @@ module Ci
after_save :update_project_statistics_after_save, if: :size_changed? after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed? after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
after_save :update_file_store after_save :update_file_store, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
......
...@@ -530,6 +530,17 @@ module Ci ...@@ -530,6 +530,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# These methods overwrite autogenerated ones to return correct results.
def unknown?
Gitlab.rails5? ? source.nil? : super
end
def unknown_source?
Gitlab.rails5? ? config_source.nil? : super
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
...@@ -13,14 +13,27 @@ module Ci ...@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id has_many :builds, foreign_key: :stage_id
validates :project, presence: true, unless: :importing? with_options unless: :importing? do
validates :pipeline, presence: true, unless: :importing? validates :project, presence: true
validates :name, presence: true, unless: :importing? validates :pipeline, presence: true
validates :name, presence: true
validates :position, presence: true
end
after_initialize do |stage| after_initialize do
self.status = DEFAULT_STATUS if self.status.nil? self.status = DEFAULT_STATUS if self.status.nil?
end end
before_validation unless: :importing? do
next if position.present?
self.position = statuses.select(:stage_idx)
.where('stage_idx IS NOT NULL')
.group(:stage_idx)
.order('COUNT(*) DESC')
.first&.stage_idx.to_i
end
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
......
...@@ -424,6 +424,12 @@ class Commit ...@@ -424,6 +424,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined # no-op but needs to be defined since #persisted? is defined
end end
def touch_later
# No-op.
# This method is called by ActiveRecord.
# We don't want to do anything for `Commit` model, so this is empty.
end
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress? def work_in_progress?
......
...@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v v =~ /\d+/ ? v.to_i : v
end end
end end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# This method overwrites the autogenerated one to return correct result.
def unknown_failure?
Gitlab.rails5? ? failure_reason.nil? : super
end
end end
...@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader mount_uploader :file, LfsObjectUploader
after_save :update_file_store after_save :update_file_store, if: :file_changed?
def update_file_store def update_file_store
# The file.object_store is set during `uploader.store!` # The file.object_store is set during `uploader.store!`
......
...@@ -910,7 +910,7 @@ class User < ActiveRecord::Base ...@@ -910,7 +910,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {}) def delete_async(deleted_by:, params: {})
block if params[:hard_delete] block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params) DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end end
def notification_service def notification_service
......
...@@ -42,6 +42,7 @@ module Ci ...@@ -42,6 +42,7 @@ module Ci
def create_stage def create_stage
Ci::Stage.create!(name: @build.stage, Ci::Stage.create!(name: @build.stage,
position: @build.stage_idx,
pipeline: @build.pipeline, pipeline: @build.pipeline,
project: @build.project) project: @build.project)
end end
......
...@@ -50,21 +50,30 @@ module MergeRequests ...@@ -50,21 +50,30 @@ module MergeRequests
end end
def commit def commit
message = params[:commit_message] || merge_request.merge_commit_message
log_info("Git merge started on JID #{merge_jid}") log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message) commit_id = try_merge
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
if commit_id
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
else
raise MergeError, 'Conflicts detected during merge'
end
raise MergeError, 'Conflicts detected during merge' unless commit_id merge_request.update!(merge_commit_sha: commit_id)
end
def try_merge
message = params[:commit_message] || merge_request.merge_commit_message
merge_request.update(merge_commit_sha: commit_id) repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::HooksService::PreReceiveError => e rescue Gitlab::Git::HooksService::PreReceiveError => e
raise MergeError, e.message handle_merge_error(log_message: e.message)
rescue StandardError => e raise MergeError, 'Something went wrong during merge pre-receive hook'
raise MergeError, "Something went wrong during merge: #{e.message}" rescue => e
handle_merge_error(log_message: e.message)
raise MergeError, 'Something went wrong during merge'
ensure ensure
merge_request.update(in_progress_merge_commit_sha: nil) merge_request.update!(in_progress_merge_commit_sha: nil)
end end
def after_merge def after_merge
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.panel .panel
.panel-heading.alert.alert-danger .panel-heading.alert.alert-danger
Last repository check Last repository check
= "(#{time_ago_in_words(@project.last_repository_check_at)} ago)" = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
failed. See failed. See
= link_to 'repocheck.log', admin_logs_path = link_to 'repocheck.log', admin_logs_path
for error messages. for error messages.
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
= tag = tag
%td %td
- if runner.contacted_at - if runner.contacted_at
#{time_ago_in_words(runner.contacted_at)} ago = time_ago_with_tooltip runner.contacted_at
- else - else
Never Never
%td.admin-runner-btn-group-cell %td.admin-runner-btn-group-cell
......
...@@ -108,4 +108,4 @@ ...@@ -108,4 +108,4 @@
%td.timestamp %td.timestamp
- if build.finished_at - if build.finished_at
%span #{time_ago_in_words build.finished_at} ago %span= time_ago_with_tooltip build.finished_at
...@@ -20,5 +20,4 @@ ...@@ -20,5 +20,4 @@
%td %td
= service.description = service.description
%td.light %td.light
= time_ago_in_words service.updated_at = time_ago_with_tooltip service.updated_at
ago
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
= email_default_heading("Hello, #{@resource.name}!") = email_default_heading("Hello, #{@resource.name}!")
%p %p
Your GitLab account has been locked due to an excessive amount of unsuccessful Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} sign in attempts. Your account will automatically unlock in #{distance_of_time_in_words(Devise.unlock_in)}
or you may click the link below to unlock now. or you may click the link below to unlock now.
#cta #cta
= link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) = link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
Hello, <%= @resource.name %>! Hello, <%= @resource.name %>!
Your GitLab account has been locked due to an excessive amount of unsuccessful Your GitLab account has been locked due to an excessive amount of unsuccessful
sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %> sign in attempts. Your account will automatically unlock in <%= distance_of_time_in_words(Devise.unlock_in) %>
or you may click the link below to unlock now. or you may click the link below to unlock now.
<%= unlock_url(@resource, unlock_token: @token) %> <%= unlock_url(@resource, unlock_token: @token) %>
- if current_user.admin? .form-group
.form-group = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
= f.label :lfs_enabled, 'Large File Storage', class: 'control-label' .col-sm-10
.col-sm-10 .checkbox
.checkbox = f.label :lfs_enabled do
= f.label :lfs_enabled do = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
= f.check_box :lfs_enabled, checked: @group.lfs_enabled? %strong
%strong Allow projects within this group to use Git LFS
Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') %br/
%br/ %span.descr This setting can be overridden in each project.
%span.descr This setting can be overridden in each project.
- if can? current_user, :admin_group, @group .form-group
.form-group = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10
.col-sm-10 .checkbox
.checkbox = f.label :require_two_factor_authentication do
= f.label :require_two_factor_authentication do = f.check_box :require_two_factor_authentication
= f.check_box :require_two_factor_authentication %strong
%strong Require all users in this group to setup Two-factor authentication
Require all users in this group to setup Two-factor authentication = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
= link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') .form-group
.form-group .col-sm-offset-2.col-sm-10
.col-sm-offset-2.col-sm-10 .checkbox
.checkbox = f.text_field :two_factor_grace_period, class: 'form-control'
= f.text_field :two_factor_grace_period, class: 'form-control' .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
...@@ -129,6 +129,17 @@ ...@@ -129,6 +129,17 @@
= link_to profile_preferences_path do = link_to profile_preferences_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Preferences') } #{ _('Preferences') }
= nav_link(controller: :active_sessions) do
= link_to profile_active_sessions_path do
.nav-icon-container
= sprite_icon('monitor-lines')
%span.nav-item-name
Active Sessions
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
= link_to profile_active_sessions_path do
%strong.fly-out-top-item-name
#{ _('Active Sessions') }
= nav_link(path: 'profiles#audit_log') do = nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do = link_to audit_log_profile_path do
.nav-icon-container .nav-icon-container
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
#js-peek{ data: { env: Peek.env, #js-peek{ data: { env: Peek.env,
request_id: Peek.request_id, request_id: Peek.request_id,
peek_url: peek_routes.results_url, peek_url: peek_routes.results_url,
profile_url: url_for(params.merge(lineprofiler: 'true')) }, profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env } class: Peek.env }
- is_current_session = active_session.current?(session)
%li
.pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
= active_session_device_type_icon(active_session)
.description.pull-left
%div
%strong= active_session.ip_address
- if is_current_session
%div This is your current session
- else
%div
Last accessed on
= l(active_session.updated_at, format: :short)
%div
%strong= active_session.browser
on
%strong= active_session.os
%div
%strong Signed in
on
= l(active_session.created_at, format: :short)
- unless is_current_session
.pull-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
- page_title 'Active Sessions'
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
.col-lg-8
.append-bottom-default
%ul.well-list
= render partial: 'profiles/active_sessions/active_session', collection: @sessions
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.files-changed-inner .files-changed-inner
.inline-parallel-buttons.hidden-xs.hidden-sm .inline-parallel-buttons.hidden-xs.hidden-sm
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' = link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle - if show_whitespace_toggle
- if current_controller?(:commit) - if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- elsif @build.has_expiring_artifacts? - elsif @build.has_expiring_artifacts?
%p.build-detail-row %p.build-detail-row
The artifacts will be removed in The artifacts will be removed in
%span= time_ago_in_words @build.artifacts_expire_at %span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts? - if @build.artifacts?
.btn-group.btn-group-justified{ role: :group } .btn-group.btn-group-justified{ role: :group }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors .hide.alert.alert-danger.mr-compare-errors
.js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
.col-md-6 .col-md-6
.panel.panel-default.panel-new-merge-request .panel.panel-default.panel-new-merge-request
.panel-heading .panel-heading
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.panel-body.clearfix .panel-body.clearfix
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :source_project_id = f.hidden_field :source_project_id
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" } = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project .dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title("Select source project") = dropdown_title("Select source project")
= dropdown_filter("Search projects") = dropdown_filter("Search projects")
...@@ -21,14 +21,12 @@ ...@@ -21,14 +21,12 @@
selected: f.object.source_project_id selected: f.object.source_project_id
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :source_branch = f.hidden_field :source_branch
= dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" } = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch.git-revision-dropdown .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title("Select source branch") = dropdown_title(_("Select source branch"))
= dropdown_filter("Search branches") = dropdown_filter(_("Search branches"))
= dropdown_content do = dropdown_content
= render 'projects/merge_requests/dropdowns/branch', = dropdown_loading
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer .panel-footer
.text-center= icon('spinner spin', class: 'js-source-loading') .text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit %ul.list-unstyled.mr_source_commit
...@@ -41,7 +39,7 @@ ...@@ -41,7 +39,7 @@
- projects = target_projects(@project) - projects = target_projects(@project)
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :target_project_id = f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project .dropdown-menu.dropdown-menu-selectable.dropdown-target-project
= dropdown_title("Select target project") = dropdown_title("Select target project")
= dropdown_filter("Search projects") = dropdown_filter("Search projects")
...@@ -51,14 +49,12 @@ ...@@ -51,14 +49,12 @@
selected: f.object.target_project_id selected: f.object.target_project_id
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :target_branch = f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" } = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown.git-revision-dropdown .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title("Select target branch") = dropdown_title(_("Select target branch"))
= dropdown_filter("Search branches") = dropdown_filter(_("Search branches"))
= dropdown_content do = dropdown_content
= render 'projects/merge_requests/dropdowns/branch', = dropdown_loading
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer .panel-footer
.text-center= icon('spinner spin', class: "js-target-loading") .text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit %ul.list-unstyled.mr_target_commit
......
%ul %ul
- projects.each do |project| - projects.each do |project|
%li %li
%a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } } %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id, 'refs-url': refs_project_path(project) } }
= project.full_path = project.full_path
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
\- \-
%td %td
- if tag.created_at - if tag.created_at
= time_ago_in_words(tag.created_at) = time_ago_with_tooltip tag.created_at
- else - else
.light .light
\- \-
......
...@@ -62,6 +62,6 @@ ...@@ -62,6 +62,6 @@
%td Last contact %td Last contact
%td %td
- if @runner.contacted_at - if @runner.contacted_at
#{time_ago_in_words(@runner.contacted_at)} ago = time_ago_with_tooltip @runner.contacted_at
- else - else
Never Never
...@@ -27,5 +27,4 @@ ...@@ -27,5 +27,4 @@
= service.description = service.description
%td.light %td.light
- if service.updated_at.present? - if service.updated_at.present?
= time_ago_in_words service.updated_at = time_ago_with_tooltip service.updated_at
ago
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
%td %td
- if trigger.last_used - if trigger.last_used
#{time_ago_in_words(trigger.last_used)} ago = time_ago_with_tooltip trigger.last_used
- else - else
Never Never
......
...@@ -35,5 +35,4 @@ ...@@ -35,5 +35,4 @@
%span.light %span.light
#{t('sherlock.finished_at')}: #{t('sherlock.finished_at')}:
%strong %strong
= time_ago_in_words(@transaction.finished_at) = time_ago_with_tooltip @transaction.finished_at
= t('sherlock.ago')
...@@ -35,8 +35,7 @@ ...@@ -35,8 +35,7 @@
= t('sherlock.seconds') = t('sherlock.seconds')
%td= trans.queries.length %td= trans.queries.length
%td %td
= time_ago_in_words(trans.finished_at) = time_ago_with_tooltip trans.finished_at
= t('sherlock.ago')
%td %td
= link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
= t('sherlock.view') = t('sherlock.view')
---
title: Fixed wrong avatar URL when the avatar is on object storage.
merge_request: 18092
author:
type: fixed
---
title: Replace time_ago_in_words with JS-based one
merge_request: 18607
author: Takuya Noguchi
type: performance
---
title: Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite
merge_request: 18580
author:
type: fixed
---
title: Replace the `project/source/markdown_render.feature` spinach test with an rspec analog
merge_request: 18525
author: "@blackst0ne"
type: other
---
title: Fix errors on pushing to an empty repository
merge_request: 18462
author:
type: fixed
---
title: Fix redirection error for applications using OpenID
merge_request: 18599
author:
type: fixed
---
title: Fix commit trailer rendering when Gravatar is disabled
merge_request:
author:
type: fixed
---
title: Display active sessions and allow the user to revoke any of it
merge_request: 17867
author: Alexis Reigel
type: added
---
title: Improve quick actions summary preview
merge_request: 18659
author: George Tsiolis
type: changed
---
title: Increase new issue metadata form margin
merge_request: 18630
author: George Tsiolis
type: fixed
---
title: Fix users not seeing labels from private groups when being a member of a child project
merge_request:
author:
type: fixed
---
title: Display only generic message on merge error to avoid exposing any potentially
sensitive or user unfriendly backend messages.
merge_request:
author:
type: fixed
---
title: Show group and project LFS settings in the interface to Owners and Masters
merge_request: 18562
author:
type: changed
---
title: Move WorkInProgress vue component
merge_request: 17536
author: George Tsiolis
type: performance
---
title: Move TimeTrackingNoTrackingPane vue component
merge_request: 18676
author: George Tsiolis
type: performance
---
title: Move SidebarTimeTracking vue component
merge_request: 18677
author: George Tsiolis
type: performance
---
title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
merge_request: 18543
author:
type: fixed
---
title: Load branches on new merge request page asynchronously
merge_request: 18315
author:
type: changed
---
title: Finish NamespaceService migration to Gitaly
merge_request:
author:
type: performance
---
title: Compute Gitlab::Git::Repository#checksum on Gitaly by default
merge_request:
author:
type: performance
...@@ -115,6 +115,7 @@ module Gitlab ...@@ -115,6 +115,7 @@ module Gitlab
config.assets.precompile << "test.css" config.assets.precompile << "test.css"
config.assets.precompile << "snippets.css" config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js" config.assets.precompile << "locale/**/app.js"
config.assets.precompile << "emoji_sprites.css"
# Import gitlab-svgs directly from vendored directory # Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist" config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
......
...@@ -15,19 +15,15 @@ cookie_key = if Rails.env.development? ...@@ -15,19 +15,15 @@ cookie_key = if Rails.env.development?
"_gitlab_session" "_gitlab_session"
end end
if Rails.env.test? sessions_config = Gitlab::Redis::SharedState.params
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
else
sessions_config = Gitlab::Redis::SharedState.params
sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
Gitlab::Application.config.session_store( Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks. :redis_store, # Using the cookie_store would enable session replay attacks.
servers: sessions_config, servers: sessions_config,
key: cookie_key, key: cookie_key,
secure: Gitlab.config.gitlab.https, secure: Gitlab.config.gitlab.https,
httponly: true, httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60, expires_in: Settings.gitlab['session_expire_delay'] * 60,
path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
) )
end
...@@ -6,4 +6,16 @@ Rails.application.configure do |config| ...@@ -6,4 +6,16 @@ Rails.application.configure do |config|
Warden::Manager.before_failure do |env, opts| Warden::Manager.before_failure do |env, opts|
Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env) Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
end end
Warden::Manager.after_authentication do |user, auth, opts|
ActiveSession.cleanup(user)
end
Warden::Manager.after_set_user only: :fetch do |user, auth, opts|
ActiveSession.set(user, auth.request)
end
Warden::Manager.before_logout do |user, auth, opts|
ActiveSession.destroy(user || auth.user, auth.request.session.id)
end
end end
...@@ -30,6 +30,7 @@ resource :profile, only: [:show, :update] do ...@@ -30,6 +30,7 @@ resource :profile, only: [:show, :update] do
put :revoke put :revoke
end end
end end
resources :active_sessions, only: [:index, :destroy]
resources :emails, only: [:index, :create, :destroy] do resources :emails, only: [:index, :create, :destroy] do
member do member do
put :resend_confirmation_instructions put :resend_confirmation_instructions
......
...@@ -161,7 +161,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -161,7 +161,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
get :diff_for_path get :diff_for_path
get :update_branches
get :branch_from get :branch_from
get :branch_to get :branch_to
end end
......
class AddTmpStagePriorityIndexToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:ci_builds, [:stage_id, :stage_idx],
where: 'stage_idx IS NOT NULL', name: 'tmp_build_stage_position_index')
end
def down
remove_concurrent_index_by_name(:ci_builds, 'tmp_build_stage_position_index')
end
end
class AddIndexToCiStage < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_stages, :position, :integer
end
end
class ScheduleStagesIndexMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateStageIndex'.freeze
BATCH_SIZE = 10000
disable_ddl_transaction!
class Stage < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_stages'
end
def up
disable_statement_timeout
Stage.all.tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
...@@ -331,6 +331,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -331,6 +331,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree add_index "ci_builds", ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
...@@ -495,6 +496,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do ...@@ -495,6 +496,7 @@ ActiveRecord::Schema.define(version: 20180425131009) do
t.string "name" t.string "name"
t.integer "status" t.integer "status"
t.integer "lock_version" t.integer "lock_version"
t.integer "position"
end end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
......
...@@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon : ...@@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon :
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced. To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
# SVG Illustrations # SVG Illustrations
......
# Active Sessions
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867)
> in GitLab 10.8.
GitLab lists all devices that have logged into your account. This allows you to
review the sessions and revoke any of it that you don't recognize.
## Listing all active sessions
1. On the upper right corner, click on your avatar and go to your **Settings**.
1. Navigate to the **Active Sessions** tab.
![Active sessions list](img/active_sessions_list.png)
## Revoking a session
1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
1. Click on **Revoke** besides a session. The current session cannot be
revoked, as this would sign you out of GitLab.
...@@ -39,6 +39,7 @@ From there, you can: ...@@ -39,6 +39,7 @@ From there, you can:
- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH - Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme) - Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience to customize your own GitLab experience
- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account - Access your audit log, a security log of important events involving your account
## Changing your username ## Changing your username
......
...@@ -251,13 +251,4 @@ It is possible to host LFS objects externally by setting a custom LFS url with ` ...@@ -251,13 +251,4 @@ It is possible to host LFS objects externally by setting a custom LFS url with `
Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project. Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project). LFS can be disabled from the [Project settings](../../user/project/settings/index.md).
```bash
curl --request PUT \
--url https://example.com/api/v4/projects/<PROJECT_ID> \
--header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
--data 'lfs_enabled=false'
```
Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
Feature: Project Source Markdown Render
Background:
Given I sign in as a user
And I own project "Delta"
And I visit markdown branch
# Tree README
@javascript
Scenario: Tree view should have correct links in README
Given I go directory which contains README file
And I click on a relative link in README
Then I should see the correct markdown
@javascript
Scenario: I browse files from markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Gitlab API in README
Then I should see correct document rendered
@javascript
Scenario: I view README in markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered
@javascript
Scenario: I view README in markdown branch to see reference links to directory
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on GitLab API doc directory in README
Then I should see correct doc/api directory rendered
@javascript
Scenario: I view README in markdown branch to see reference links to file
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Maintenance in README
Then I should see correct maintenance file rendered
@javascript
Scenario: README headers should have header links
Then I should see rendered README which contains correct links
And Header "Application details" should have correct id and link
# Blob
@javascript
Scenario: I navigate to doc directory to view documentation in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on users in doc/api/README
Then I should see the correct document file
@javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And I see correct file rendered
And I click on raketasks in doc/api/README
Then I should see correct directory rendered
@javascript
Scenario: I navigate to doc directory to view user doc in markdown
And I navigate to the doc/api/README
And Header "GitLab API" should have correct id and link
# Markdown branch
@javascript
Scenario: I browse files from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
And I should see rendered README which contains correct links
And I click on Gitlab API in README
Then I should see correct document rendered for markdown branch
@javascript
Scenario: I browse directory from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered for markdown branch
@javascript
Scenario: I navigate to doc directory to view documentation in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
And I see correct file rendered in markdown branch
And I click on users in doc/api/README
Then I should see the users document file in markdown branch
@javascript
Scenario: I navigate to doc directory to view user doc in markdown branch
When I visit markdown branch
And I navigate to the doc/api/README
And I see correct file rendered in markdown branch
And I click on raketasks in doc/api/README
Then I should see correct directory rendered for markdown branch
@javascript
Scenario: Tree markdown links view empty urls should have correct urls
When I visit markdown branch
Then The link with text "empty" should have url "tree/markdown"
When I visit markdown branch "README.md" blob
Then The link with text "empty" should have url "blob/markdown/README.md"
When I visit markdown branch "d" tree
Then The link with text "empty" should have url "tree/markdown/d"
When I visit markdown branch "d/README.md" blob
Then The link with text "empty" should have url "blob/markdown/d/README.md"
# "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
# which Spinach interprets as the start of a comment.
@javascript
Scenario: All markdown links with ids should have correct urls
When I visit markdown branch
Then The link with text "ID" should have url "tree/markdownID"
Then The link with text "/ID" should have url "tree/markdownID"
Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
When I visit markdown branch "README.md" blob
Then The link with text "ID" should have url "blob/markdown/README.mdID"
Then The link with text "/ID" should have url "blob/markdown/README.mdID"
Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
# Wiki
Scenario: I create a wiki page with different links
Given I go to wiki page
And I add various links to the wiki page
Then Wiki page should have added links
And I click on test link
Then I see new wiki page named test
When I go back to wiki page home
And I click on GitLab API doc link
Then I see Gitlab API document
When I go back to wiki page home
And I click on Rake tasks link
Then I see Rake tasks directory
Scenario: Wiki headers should have should have ids generated for them.
Given I go to wiki page
And I add a header to the wiki page
Then Wiki header should have correct id and link
...@@ -53,7 +53,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps ...@@ -53,7 +53,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
first('.js-source-branch').click first('.js-source-branch').click
wait_for_requests wait_for_requests
first('.dropdown-source-branch .dropdown-content a', text: 'fix').click first('.js-source-branch-dropdown .dropdown-content a', text: 'fix').click
click_button "Compare branches and continue" click_button "Compare branches and continue"
......
# If you need to modify the existing seed repository for your tests,
# it is recommended that you make the changes on the `markdown` branch of the seed project repository,
# which should only be used by tests in this file. See `/spec/factories.rb#project` for more info.
class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedMarkdown
include WaitForRequests
step 'I own project "Delta"' do
@project = ::Project.find_by(name: "Delta")
@project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.add_master(@user)
end
step 'I should see files from repository in markdown' do
expect(current_path).to eq project_tree_path(@project, "markdown")
expect(page).to have_content "README.md"
expect(page).to have_content "CHANGELOG"
end
step 'I should see rendered README which contains correct links' do
expect(page).to have_content "Welcome to GitLab GitLab is a free project and repository management application"
expect(page).to have_link "GitLab API doc"
expect(page).to have_link "GitLab API website"
expect(page).to have_link "Rake tasks"
expect(page).to have_link "backup and restore procedure"
expect(page).to have_link "GitLab API doc directory"
expect(page).to have_link "Maintenance"
end
step 'I click on Gitlab API in README' do
click_link "GitLab API doc"
end
step 'I should see correct document rendered' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
wait_for_requests
expect(page).to have_content "All API requests require authentication"
end
step 'I click on Rake tasks in README' do
click_link "Rake tasks"
end
step 'I should see correct directory rendered' do
expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
expect(page).to have_content "backup_restore.md"
expect(page).to have_content "maintenance.md"
end
step 'I click on GitLab API doc directory in README' do
click_link "GitLab API doc directory"
end
step 'I should see correct doc/api directory rendered' do
expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
expect(page).to have_content "README.md"
expect(page).to have_content "users.md"
end
step 'I click on Maintenance in README' do
click_link "Maintenance"
end
step 'I should see correct maintenance file rendered' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/raketasks/maintenance.md")
wait_for_requests
expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
end
step 'I click on link "empty" in the README' do
page.within('.readme-holder') do
click_link "empty"
end
end
step 'I click on link "id" in the README' do
page.within('.readme-holder') do
click_link "#id"
end
end
step 'I navigate to the doc/api/README' do
page.within '.tree-table' do
click_link "doc"
end
page.within '.tree-table' do
click_link "api"
end
wait_for_requests
page.within '.tree-table' do
click_link "README.md"
end
end
step 'I see correct file rendered' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
wait_for_requests
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
end
step 'I click on users in doc/api/README' do
click_link "Users"
end
step 'I should see the correct document file' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
expect(page).to have_content "Get a list of users."
end
step 'I click on raketasks in doc/api/README' do
click_link "Rake tasks"
end
# Markdown branch
When 'I visit markdown branch' do
visit project_tree_path(@project, "markdown")
wait_for_requests
end
When 'I visit markdown branch "README.md" blob' do
visit project_blob_path(@project, "markdown/README.md")
end
When 'I visit markdown branch "d" tree' do
visit project_tree_path(@project, "markdown/d")
end
When 'I visit markdown branch "d/README.md" blob' do
visit project_blob_path(@project, "markdown/d/README.md")
end
step 'I should see files from repository in markdown branch' do
expect(current_path).to eq project_tree_path(@project, "markdown")
expect(page).to have_content "README.md"
expect(page).to have_content "CHANGELOG"
end
step 'I see correct file rendered in markdown branch' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
wait_for_requests
expect(page).to have_content "Contents"
expect(page).to have_link "Users"
expect(page).to have_link "Rake tasks"
end
step 'I should see correct document rendered for markdown branch' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
wait_for_requests
expect(page).to have_content "All API requests require authentication"
end
step 'I should see correct directory rendered for markdown branch' do
expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
expect(page).to have_content "backup_restore.md"
expect(page).to have_content "maintenance.md"
end
step 'I should see the users document file in markdown branch' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
expect(page).to have_content "Get a list of users."
end
# Expected link contents
step 'The link with text "empty" should have url "tree/markdown"' do
wait_for_requests
find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown")
end
step 'The link with text "empty" should have url "blob/markdown/README.md"' do
find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md")
end
step 'The link with text "empty" should have url "tree/markdown/d"' do
find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown/d")
end
step 'The link with text "empty" should have '\
'url "blob/markdown/d/README.md"' do
find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/d/README.md")
end
step 'The link with text "ID" should have url "tree/markdownID"' do
find('a', text: /^#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
end
step 'The link with text "/ID" should have url "tree/markdownID"' do
find('a', text: %r{^/#id$})['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
end
step 'The link with text "README.mdID" '\
'should have url "blob/markdown/README.mdID"' do
find('a', text: /^README.md#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
end
step 'The link with text "d/README.mdID" should have '\
'url "blob/markdown/d/README.mdID"' do
find('a', text: %r{^d/README.md#id$})['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id'
end
step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
wait_for_requests
find('a', text: /^#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
end
step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do
find('a', text: %r{^/#id$})['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
end
# Wiki
step 'I go to wiki page' do
first(:link, "Wiki").click
expect(current_path).to eq project_wiki_path(@project, "home")
end
step 'I add various links to the wiki page' do
fill_in "wiki[content]", with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n"
fill_in "wiki[message]", with: "Adding links to wiki"
page.within '.wiki-form' do
click_button "Create page"
end
end
step 'Wiki page should have added links' do
expect(current_path).to eq project_wiki_path(@project, "home")
expect(page).to have_content "test GitLab API doc Rake tasks"
end
step 'I add a header to the wiki page' do
fill_in "wiki[content]", with: "# Wiki header\n"
fill_in "wiki[message]", with: "Add header to wiki"
page.within '.wiki-form' do
click_button "Create page"
end
end
step 'Wiki header should have correct id and link' do
header_should_have_correct_id_and_link(1, 'Wiki header', 'wiki-header')
end
step 'I click on test link' do
click_link "test"
end
step 'I see new wiki page named test' do
expect(current_path).to eq project_wiki_path(@project, "test")
page.within(:css, ".nav-text") do
expect(page).to have_content "Test"
expect(page).to have_content "Create Page"
end
end
When 'I go back to wiki page home' do
visit project_wiki_path(@project, "home")
expect(current_path).to eq project_wiki_path(@project, "home")
end
step 'I click on GitLab API doc link' do
click_link "GitLab API"
end
step 'I see Gitlab API document' do
expect(current_path).to eq project_wiki_path(@project, "api")
page.within(:css, ".nav-text") do
expect(page).to have_content "Create"
expect(page).to have_content "Api"
end
end
step 'I click on Rake tasks link' do
click_link "Rake tasks"
end
step 'I see Rake tasks directory' do
expect(current_path).to eq project_wiki_path(@project, "raketasks")
page.within(:css, ".nav-text") do
expect(page).to have_content "Create"
expect(page).to have_content "Rake"
end
end
step 'I go directory which contains README file' do
visit project_tree_path(@project, "markdown/doc/api")
expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
end
step 'I click on a relative link in README' do
click_link "Users"
end
step 'I should see the correct markdown' do
expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
wait_for_requests
expect(page).to have_content "List users"
end
step 'Header "Application details" should have correct id and link' do
wait_for_requests
header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
end
step 'Header "GitLab API" should have correct id and link' do
header_should_have_correct_id_and_link(1, 'GitLab API', 'gitlab-api')
end
end
module SharedMarkdown module SharedMarkdown
include Spinach::DSL include Spinach::DSL
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
node = find("#{parent} h#{level} a#user-content-#{id}")
expect(node[:href]).to end_with "##{id}"
# Work around a weird Capybara behavior where calling `parent` on a node
# returns the whole document, not the node's actual parent element
expect(find(:xpath, "#{node.path}/..").text).to eq text
end
step 'I should not see the Markdown preview' do step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible expect(find('.gfm-form .js-md-preview')).not_to be_visible
end end
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateStageIndex
def perform(start_id, stop_id)
migrate_stage_index_sql(start_id.to_i, stop_id.to_i).tap do |sql|
ActiveRecord::Base.connection.execute(sql)
end
end
private
def migrate_stage_index_sql(start_id, stop_id)
if Gitlab::Database.postgresql?
<<~SQL
WITH freqs AS (
SELECT stage_id, stage_idx, COUNT(*) AS freq FROM ci_builds
WHERE stage_id BETWEEN #{start_id} AND #{stop_id}
AND stage_idx IS NOT NULL
GROUP BY stage_id, stage_idx
), indexes AS (
SELECT DISTINCT stage_id, last_value(stage_idx)
OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
FROM freqs
)
UPDATE ci_stages SET position = indexes.index
FROM indexes WHERE indexes.stage_id = ci_stages.id
AND ci_stages.position IS NULL;
SQL
else
<<~SQL
UPDATE ci_stages
SET position =
(SELECT stage_idx FROM ci_builds
WHERE ci_builds.stage_id = ci_stages.id
GROUP BY ci_builds.stage_idx ORDER BY COUNT(*) DESC LIMIT 1)
WHERE ci_stages.id BETWEEN #{start_id} AND #{stop_id}
AND ci_stages.position IS NULL
SQL
end
end
end
end
end
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC') def initialize(cron, cron_timezone = 'UTC')
@cron = cron @cron = cron
@cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name @cron_timezone = timezone_name(cron_timezone)
end end
def next_time_from(time) def next_time_from(time)
...@@ -24,6 +24,12 @@ module Gitlab ...@@ -24,6 +24,12 @@ module Gitlab
private private
def timezone_name(timezone)
ActiveSupport::TimeZone.find_tzinfo(timezone).name
rescue TZInfo::InvalidTimezoneIdentifier
timezone
end
# NOTE: # NOTE:
# cron_timezone can only accept timezones listed in TZInfo::Timezone. # cron_timezone can only accept timezones listed in TZInfo::Timezone.
# Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted, # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
def attributes def attributes
{ name: @attributes.fetch(:name), { name: @attributes.fetch(:name),
position: @attributes.fetch(:index),
pipeline: @pipeline, pipeline: @pipeline,
project: @pipeline.project } project: @pipeline.project }
end end
......
module Gitlab
module Database
module ArelMethods
private
# In Arel 7.0.0 (Arel 7.1.4 is used in Rails 5.0) the `engine` parameter of `Arel::UpdateManager#initializer`
# was removed.
# Remove this file and inline this method when removing rails5? code.
def arel_update_manager
if Gitlab.rails5?
Arel::UpdateManager.new
else
Arel::UpdateManager.new(ActiveRecord::Base)
end
end
end
end
end
module Gitlab module Gitlab
module Database module Database
module MigrationHelpers module MigrationHelpers
include Gitlab::Database::ArelMethods
BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
...@@ -314,7 +316,7 @@ module Gitlab ...@@ -314,7 +316,7 @@ module Gitlab
stop_arel = yield table, stop_arel if block_given? stop_arel = yield table, stop_arel if block_given?
stop_row = exec_query(stop_arel.to_sql).to_hash.first stop_row = exec_query(stop_arel.to_sql).to_hash.first
update_arel = Arel::UpdateManager.new(ActiveRecord::Base) update_arel = arel_update_manager
.table(table) .table(table)
.set([[table[column], value]]) .set([[table[column], value]])
.where(table[:id].gteq(start_id)) .where(table[:id].gteq(start_id))
......
...@@ -3,6 +3,8 @@ module Gitlab ...@@ -3,6 +3,8 @@ module Gitlab
module RenameReservedPathsMigration module RenameReservedPathsMigration
module V1 module V1
class RenameBase class RenameBase
include Gitlab::Database::ArelMethods
attr_reader :paths, :migration attr_reader :paths, :migration
delegate :update_column_in_batches, delegate :update_column_in_batches,
...@@ -62,10 +64,10 @@ module Gitlab ...@@ -62,10 +64,10 @@ module Gitlab
old_full_path, old_full_path,
new_full_path) new_full_path)
update = Arel::UpdateManager.new(ActiveRecord::Base) update = arel_update_manager
.table(routes) .table(routes)
.set([[routes[:path], replace_statement]]) .set([[routes[:path], replace_statement]])
.where(Arel::Nodes::SqlLiteral.new(filter)) .where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql) execute(update.to_sql)
end end
......
...@@ -1569,7 +1569,8 @@ module Gitlab ...@@ -1569,7 +1569,8 @@ module Gitlab
end end
def checksum def checksum
gitaly_migrate(:calculate_checksum) do |is_enabled| gitaly_migrate(:calculate_checksum,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_repository_client.calculate_checksum gitaly_repository_client.calculate_checksum
else else
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.test_env = Rails.env.test? gon.test_env = Rails.env.test?
gon.suggested_label_colors = LabelsHelper.suggested_colors gon.suggested_label_colors = LabelsHelper.suggested_colors
......
...@@ -5,6 +5,8 @@ module Gitlab ...@@ -5,6 +5,8 @@ module Gitlab
module Redis module Redis
class SharedState < ::Gitlab::Redis::Wrapper class SharedState < ::Gitlab::Redis::Wrapper
SESSION_NAMESPACE = 'session:gitlab'.freeze SESSION_NAMESPACE = 'session:gitlab'.freeze
USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
......
...@@ -294,17 +294,7 @@ module Gitlab ...@@ -294,17 +294,7 @@ module Gitlab
# add_namespace("default", "gitlab") # add_namespace("default", "gitlab")
# #
def add_namespace(storage, name) def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace, Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
else
path = full_path(storage, name)
FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
end
end
rescue Errno::EEXIST => e
Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
rescue GRPC::InvalidArgument => e rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message raise ArgumentError, e.message
end end
...@@ -316,14 +306,7 @@ module Gitlab ...@@ -316,14 +306,7 @@ module Gitlab
# rm_namespace("default", "gitlab") # rm_namespace("default", "gitlab")
# #
def rm_namespace(storage, name) def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace, Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
else
FileUtils.rm_r(full_path(storage, name), force: true)
end
end
rescue GRPC::InvalidArgument => e rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message raise ArgumentError, e.message
end end
...@@ -335,17 +318,7 @@ module Gitlab ...@@ -335,17 +318,7 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
# #
def mv_namespace(storage, old_name, new_name) def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient.migrate(:rename_namespace, Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name)
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
Gitlab::GitalyClient::NamespaceService.new(storage)
.rename(old_name, new_name)
else
break false if exists?(storage, new_name) || !exists?(storage, old_name)
FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
end
end
rescue GRPC::InvalidArgument rescue GRPC::InvalidArgument
false false
end end
...@@ -370,17 +343,8 @@ module Gitlab ...@@ -370,17 +343,8 @@ module Gitlab
# exists?(storage, 'gitlab') # exists?(storage, 'gitlab')
# exists?(storage, 'gitlab/cookies.git') # exists?(storage, 'gitlab/cookies.git')
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name) def exists?(storage, dir_name)
Gitlab::GitalyClient.migrate(:namespace_exists, Gitlab::GitalyClient::NamespaceService.new(storage).exists?(dir_name)
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled
Gitlab::GitalyClient::NamespaceService.new(storage)
.exists?(dir_name)
else
File.exist?(full_path(storage, dir_name))
end
end
end end
protected protected
......
...@@ -142,7 +142,7 @@ describe Projects::Clusters::GcpController do ...@@ -142,7 +142,7 @@ describe Projects::Clusters::GcpController do
context 'when google project billing is enabled' do context 'when google project billing is enabled' do
before do before do
redis_double = double redis_double = double.as_null_object
allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
end end
......
...@@ -157,34 +157,4 @@ describe Projects::MergeRequests::CreationsController do ...@@ -157,34 +157,4 @@ describe Projects::MergeRequests::CreationsController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
end end
describe 'GET #update_branches' do
before do
allow(Ability).to receive(:allowed?).and_call_original
end
it 'lists the branches of another fork if the user has access' do
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
get :update_branches,
namespace_id: fork_project.namespace,
project_id: fork_project,
target_project_id: project.id
expect(assigns(:target_branches)).not_to be_empty
expect(response).to have_gitlab_http_status(200)
end
it 'does not list branches when the user cannot read the project' do
expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
get :update_branches,
namespace_id: fork_project.namespace,
project_id: fork_project,
target_project_id: project.id
expect(response).to have_gitlab_http_status(200)
expect(assigns(:target_branches)).to eq([])
end
end
end end
...@@ -54,9 +54,9 @@ describe Projects::RawController do ...@@ -54,9 +54,9 @@ describe Projects::RawController do
end end
context 'and lfs uses object storage' do context 'and lfs uses object storage' do
let(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
before do before do
lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
lfs_object.save!
stub_lfs_object_storage stub_lfs_object_storage
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end end
......
...@@ -21,6 +21,7 @@ FactoryBot.define do ...@@ -21,6 +21,7 @@ FactoryBot.define do
pipeline factory: :ci_empty_pipeline pipeline factory: :ci_empty_pipeline
name 'test' name 'test'
position 1
status 'pending' status 'pending'
end end
end end
...@@ -2,6 +2,7 @@ FactoryBot.define do ...@@ -2,6 +2,7 @@ FactoryBot.define do
factory :commit_status, class: CommitStatus do factory :commit_status, class: CommitStatus do
name 'default' name 'default'
stage 'test' stage 'test'
stage_idx 0
status 'success' status 'success'
description 'commit status' description 'commit status'
pipeline factory: :ci_pipeline_with_one_job pipeline factory: :ci_pipeline_with_one_job
......
...@@ -19,7 +19,7 @@ feature 'Admin uses repository checks' do ...@@ -19,7 +19,7 @@ feature 'Admin uses repository checks' do
expect(page).to have_content('Repository check was triggered') expect(page).to have_content('Repository check was triggered')
end end
scenario 'to see a single failed repository check' do scenario 'to see a single failed repository check', :js do
project = create(:project) project = create(:project)
project.update_columns( project.update_columns(
last_repository_check_failed: true, last_repository_check_failed: true,
......
...@@ -178,9 +178,10 @@ feature 'Issues > User uses quick actions', :js do ...@@ -178,9 +178,10 @@ feature 'Issues > User uses quick actions', :js do
end end
context 'when the project is valid but the user not authorized' do context 'when the project is valid but the user not authorized' do
let(:project_unauthorized) {create(:project, :public)} let(:project_unauthorized) { create(:project, :public) }
before do before do
gitlab_sign_out
sign_in(user) sign_in(user)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end
...@@ -195,6 +196,7 @@ feature 'Issues > User uses quick actions', :js do ...@@ -195,6 +196,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the project is invalid' do context 'when the project is invalid' do
before do before do
gitlab_sign_out
sign_in(user) sign_in(user)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end
......
...@@ -19,7 +19,7 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -19,7 +19,7 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch') expect(page).to have_content('Target branch')
first('.js-source-branch').click first('.js-source-branch').click
find('.dropdown-source-branch .dropdown-content a', match: :first).click find('.js-source-branch-dropdown .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3" expect(page).to have_content "b83d6e3"
end end
...@@ -35,22 +35,16 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -35,22 +35,16 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch') expect(page).to have_content('Target branch')
first('.js-target-branch').click first('.js-target-branch').click
find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3" expect(page).to have_content "b83d6e3"
end end
it 'generates a diff for an orphaned branch' do it 'generates a diff for an orphaned branch' do
visit project_merge_requests_path(project) visit project_new_merge_request_path(project)
page.within '.content' do
click_link 'New merge request'
end
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
find('.js-source-branch', match: :first).click find('.js-source-branch', match: :first).click
find('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch', match: :first).click find('.js-source-branch-dropdown .dropdown-content a', text: 'orphaned-branch', match: :first).click
click_button "Compare branches" click_button "Compare branches"
click_link "Changes" click_link "Changes"
...@@ -71,19 +65,18 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -71,19 +65,18 @@ describe 'Merge request > User selects branches for new MR', :js do
first('.js-source-branch').click first('.js-source-branch').click
input = find('.dropdown-source-branch .dropdown-input-field') page.within '.js-source-branch-dropdown' do
input.click input = find('.dropdown-input-field')
input.send_keys('orphaned-branch') input.click
input.send_keys('orphaned-branch')
find('.dropdown-source-branch .dropdown-content li', match: :first) expect(page).to have_css('.dropdown-content li', count: 1)
source_items = all('.dropdown-source-branch .dropdown-content li') end
expect(source_items.count).to eq(1)
first('.js-target-branch').click first('.js-target-branch').click
find('.dropdown-target-branch .dropdown-content li', match: :first) find('.js-target-branch-dropdown .dropdown-content li', match: :first)
target_items = all('.dropdown-target-branch .dropdown-content li') target_items = all('.js-target-branch-dropdown .dropdown-content li')
expect(target_items.count).to be > 1 expect(target_items.count).to be > 1
end end
...@@ -171,7 +164,6 @@ describe 'Merge request > User selects branches for new MR', :js do ...@@ -171,7 +164,6 @@ describe 'Merge request > User selects branches for new MR', :js do
page.within('.merge-request') do page.within('.merge-request') do
click_link 'Pipelines' click_link 'Pipelines'
wait_for_requests
expect(page).to have_content "##{pipeline.id}" expect(page).to have_content "##{pipeline.id}"
end end
......
require 'rails_helper'
feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
let(:user) do
create(:user).tap do |user|
user.current_sign_in_at = Time.current
end
end
around do |example|
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
example.run
end
end
scenario 'User sees their active sessions' do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
# note: headers can only be set on the non-js (aka. rack-test) driver
using_session :session1 do
Capybara.page.driver.header(
'User-Agent',
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0'
)
gitlab_sign_in(user)
end
# set an additional session on another device
using_session :session2 do
Capybara.page.driver.header(
'User-Agent',
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]'
)
gitlab_sign_in(user)
end
using_session :session1 do
visit profile_active_sessions_path
expect(page).to have_content(
'127.0.0.1 ' \
'This is your current session ' \
'Firefox on Ubuntu ' \
'Signed in on 12 Mar 09:06'
)
expect(page).to have_selector '[title="Desktop"]', count: 1
expect(page).to have_content(
'127.0.0.1 ' \
'Last accessed on 12 Mar 09:06 ' \
'Mobile Safari on iOS ' \
'Signed in on 12 Mar 09:06'
)
expect(page).to have_selector '[title="Smartphone"]', count: 1
end
end
scenario 'User can revoke a session', :js, :redis_session_store do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
# set an additional session in another browser
using_session :session2 do
gitlab_sign_in(user)
end
using_session :session1 do
gitlab_sign_in(user)
visit profile_active_sessions_path
expect(page).to have_link('Revoke', count: 1)
accept_confirm { click_on 'Revoke' }
expect(page).not_to have_link('Revoke')
end
using_session :session2 do
visit profile_active_sessions_path
expect(page).to have_content('You need to sign in or sign up before continuing.')
end
end
end
require 'spec_helper' require "spec_helper"
describe 'Projects > Files > User browses files' do describe "User browses files" do
let(:fork_message) do let(:fork_message) do
"You're not allowed to make changes to this project directly. "\ "You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request." "A fork of this project has been created that you can make changes in, so you can submit a merge request."
end end
let(:project) { create(:project, :repository, name: 'Shop') } let(:project) { create(:project, :repository, name: "Shop") }
let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project2) { create(:project, :repository, name: "Another Project", path: "another-project") }
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
let(:tree_path_ref_6d39438) { project_tree_path(project, '6d39438') }
let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:user) { project.owner } let(:user) { project.owner }
...@@ -16,57 +14,55 @@ describe 'Projects > Files > User browses files' do ...@@ -16,57 +14,55 @@ describe 'Projects > Files > User browses files' do
sign_in(user) sign_in(user)
end end
it 'shows last commit for current directory' do it "shows last commit for current directory" do
visit(tree_path_root_ref) visit(tree_path_root_ref)
click_link 'files' click_link("files")
last_commit = project.repository.last_commit_for_path(project.default_branch, 'files') last_commit = project.repository.last_commit_for_path(project.default_branch, "files")
page.within('.blob-commit-info') do
expect(page).to have_content last_commit.short_id page.within(".blob-commit-info") do
expect(page).to have_content last_commit.author_name expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name)
end end
end end
context 'when browsing the master branch' do context "when browsing the master branch" do
before do before do
visit(tree_path_root_ref) visit(tree_path_root_ref)
end end
it 'shows files from a repository' do it "shows files from a repository" do
expect(page).to have_content('VERSION') expect(page).to have_content("VERSION")
expect(page).to have_content('.gitignore') .and have_content(".gitignore")
expect(page).to have_content('LICENSE') .and have_content("LICENSE")
end end
it 'shows the "Browse Directory" link' do it "shows the `Browse Directory` link" do
click_link('files') click_link("files")
click_link('History') click_link("History")
expect(page).to have_link('Browse Directory') expect(page).to have_link("Browse Directory").and have_no_link("Browse Code")
expect(page).not_to have_link('Browse Code')
end end
it 'shows the "Browse File" link' do it "shows the `Browse File` link" do
page.within('.tree-table') do page.within(".tree-table") do
click_link('README.md') click_link("README.md")
end end
click_link('History')
expect(page).to have_link('Browse File') click_link("History")
expect(page).not_to have_link('Browse Files')
expect(page).to have_link("Browse File").and have_no_link("Browse Files")
end end
it 'shows the "Browse Files" link' do it "shows the `Browse Files` link" do
click_link('History') click_link("History")
expect(page).to have_link('Browse Files') expect(page).to have_link("Browse Files").and have_no_link("Browse Directory")
expect(page).not_to have_link('Browse Directory')
end end
it 'redirects to the permalink URL' do it "redirects to the permalink URL" do
click_link('.gitignore') click_link(".gitignore")
click_link('Permalink') click_link("Permalink")
permalink_path = project_blob_path(project, "#{project.repository.commit.sha}/.gitignore") permalink_path = project_blob_path(project, "#{project.repository.commit.sha}/.gitignore")
...@@ -74,80 +70,180 @@ describe 'Projects > Files > User browses files' do ...@@ -74,80 +70,180 @@ describe 'Projects > Files > User browses files' do
end end
end end
context 'when browsing a specific ref' do context "when browsing the `markdown` branch", :js do
context "when browsing the root" do
before do
visit(project_tree_path(project, "markdown"))
end
it "shows correct files and links" do
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown")
find("a", text: /^#id$/)["href"] == project_tree_url(project, "markdown", anchor: "#id")
find("a", text: %r{^/#id$})["href"] == project_tree_url(project, "markdown", anchor: "#id")
find("a", text: /^README.md#id$/)["href"] == project_blob_url(project, "markdown/README.md", anchor: "#id")
find("a", text: %r{^d/README.md#id$})["href"] == project_blob_url(project, "d/markdown/README.md", anchor: "#id")
# rubocop:enable Lint/Void
expect(current_path).to eq(project_tree_path(project, "markdown"))
expect(page).to have_content("README.md")
.and have_content("CHANGELOG")
.and have_content("Welcome to GitLab GitLab is a free project and repository management application")
.and have_link("GitLab API doc")
.and have_link("GitLab API website")
.and have_link("Rake tasks")
.and have_link("backup and restore procedure")
.and have_link("GitLab API doc directory")
.and have_link("Maintenance")
.and have_header_with_correct_id_and_link(2, "Application details", "application-details")
end
it "shows correct content of file" do
click_link("GitLab API doc")
expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/README.md"))
expect(page).to have_content("All API requests require authentication")
.and have_content("Contents")
.and have_link("Users")
.and have_link("Rake tasks")
.and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
click_link("Users")
expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
expect(page).to have_content("Get a list of users.")
page.go_back
click_link("Rake tasks")
expect(current_path).to eq(project_tree_path(project, "markdown/doc/raketasks"))
expect(page).to have_content("backup_restore.md").and have_content("maintenance.md")
click_link("shop")
click_link("Maintenance")
expect(current_path).to eq(project_blob_path(project, "markdown/doc/raketasks/maintenance.md"))
expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production")
click_link("shop")
page.within(".tree-table") do
click_link("README.md")
end
page.go_back
page.within(".tree-table") do
click_link("d")
end
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown/d")
# rubocop:enable Lint/Void
page.within(".tree-table") do
click_link("README.md")
end
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
# rubocop:enable Lint/Void
end
it "shows correct content of directory" do
click_link("GitLab API doc directory")
expect(current_path).to eq(project_tree_path(project, "markdown/doc/api"))
expect(page).to have_content("README.md").and have_content("users.md")
click_link("Users")
expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
expect(page).to have_content("List users").and have_content("Get a list of users.")
end
end
end
context "when browsing a specific ref" do
let(:ref) { project_tree_path(project, "6d39438") }
before do before do
visit(tree_path_ref_6d39438) visit(ref)
end end
it 'shows files from a repository for "6d39438"' do it "shows files from a repository for `6d39438`" do
expect(current_path).to eq(tree_path_ref_6d39438) expect(current_path).to eq(ref)
expect(page).to have_content('.gitignore') expect(page).to have_content(".gitignore").and have_content("LICENSE")
expect(page).to have_content('LICENSE')
end end
it 'shows files from a repository with apostroph in its name', :js do it "shows files from a repository with apostroph in its name", :js do
first('.js-project-refs-dropdown').click first(".js-project-refs-dropdown").click
page.within('.project-refs-form') do page.within(".project-refs-form") do
click_link("'test'") click_link("'test'")
end end
expect(page).to have_selector('.dropdown-toggle-text', text: "'test'") expect(page).to have_selector(".dropdown-toggle-text", text: "'test'")
visit(project_tree_path(project, "'test'")) visit(project_tree_path(project, "'test'"))
expect(page).to have_css('.tree-commit-link', visible: true) expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
expect(page).not_to have_content('Loading commit data...')
end end
it 'shows the code with a leading dot in the directory', :js do it "shows the code with a leading dot in the directory", :js do
first('.js-project-refs-dropdown').click first(".js-project-refs-dropdown").click
page.within('.project-refs-form') do page.within(".project-refs-form") do
click_link('fix') click_link("fix")
end end
visit(project_tree_path(project, 'fix/.testdir')) visit(project_tree_path(project, "fix/.testdir"))
expect(page).to have_css('.tree-commit-link', visible: true) expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
expect(page).not_to have_content('Loading commit data...')
end end
it 'does not show the permalink link' do it "does not show the permalink link" do
click_link('.gitignore') click_link(".gitignore")
expect(page).not_to have_link('permalink') expect(page).not_to have_link("permalink")
end end
end end
context 'when browsing a file content' do context "when browsing a file content" do
before do before do
visit(tree_path_root_ref) visit(tree_path_root_ref)
click_link('.gitignore')
click_link(".gitignore")
end end
it 'shows a file content', :js do it "shows a file content", :js do
wait_for_requests expect(page).to have_content("*.rbc")
expect(page).to have_content('*.rbc')
end end
it 'is possible to blame' do it "is possible to blame" do
click_link 'Blame' click_link("Blame")
expect(page).to have_content "*.rb" expect(page).to have_content("*.rb")
expect(page).to have_content "Dmitriy Zaporozhets" .and have_content("Dmitriy Zaporozhets")
expect(page).to have_content "Initial commit" .and have_content("Initial commit")
end end
end end
context 'when browsing a raw file' do context "when browsing a raw file" do
before do before do
visit(project_blob_path(project, File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path))) path = File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)
visit(project_blob_path(project, path))
end end
it 'shows a raw file content' do it "shows a raw file content" do
click_link('Open raw') click_link("Open raw")
expect(source).to eq('') # Body is filled in by gitlab-workhorse
expect(source).to eq("") # Body is filled in by gitlab-workhorse
end end
end end
end end
require 'rails_helper' require 'rails_helper'
describe 'Projects > Settings > LFS settings' do describe 'Projects > Settings > LFS settings' do
let(:admin) { create(:admin) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :master }
context 'LFS enabled setting' do context 'LFS enabled setting' do
before do before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
sign_in(admin) sign_in(user)
project.add_role(user, role)
end end
it 'displays the correct elements', :js do context 'for master' do
visit edit_project_path(project) let(:role) { :master }
expect(page).to have_content('Git Large File Storage') it 'displays the correct elements', :js do
expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true) visit edit_project_path(project)
expect(page).to have_content('Git Large File Storage')
expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
end
end end
end end
end end
require 'spec_helper' require "spec_helper"
describe 'User creates wiki page' do describe "User creates wiki page" do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
...@@ -10,67 +10,104 @@ describe 'User creates wiki page' do ...@@ -10,67 +10,104 @@ describe 'User creates wiki page' do
visit(project_wikis_path(project)) visit(project_wikis_path(project))
end end
context 'when wiki is empty' do context "when wiki is empty" do
context 'in a user namespace' do context "in a user namespace" do
let(:project) { create(:project, namespace: user.namespace) } let(:project) { create(:project, namespace: user.namespace) }
it 'shows validation error message' do it "shows validation error message" do
page.within('.wiki-form') do page.within(".wiki-form") do
fill_in(:wiki_content, with: '') fill_in(:wiki_content, with: "")
click_on('Create page')
click_on("Create page")
end end
expect(page).to have_content('The form contains the following error:') expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank")
expect(page).to have_content("Content can't be blank")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "[link test](test)")
page.within('.wiki-form') do click_on("Create page")
fill_in(:wiki_content, with: '[link test](test)')
click_on('Create page')
end end
expect(page).to have_content('Home') expect(page).to have_content("Home").and have_content("link test")
expect(page).to have_content('link test')
click_link('link test') click_link("link test")
expect(page).to have_content('Create Page') expect(page).to have_content("Create Page")
end end
it 'shows non-escaped link in the pages list', :js do it "shows non-escaped link in the pages list", :js do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'one/two/three-test') fill_in(:new_wiki_path, with: "one/two/three-test")
click_on('Create page')
click_on("Create page")
end end
page.within('.wiki-form') do page.within(".wiki-form") do
fill_in(:wiki_content, with: 'wiki content') fill_in(:wiki_content, with: "wiki content")
click_on('Create page')
click_on("Create page")
end end
expect(current_path).to include('one/two/three-test') expect(current_path).to include("one/two/three-test")
expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end end
it 'has "Create home" as a commit message' do it "has `Create home` as a commit message" do
expect(page).to have_field('wiki[message]', with: 'Create home') expect(page).to have_field("wiki[message]", with: "Create home")
end end
it 'creates a page from the home page' do it "creates a page from the home page" do
fill_in(:wiki_content, with: 'My awesome wiki!') fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n")
fill_in(:wiki_message, with: "Adding links to wiki")
page.within(".wiki-form") do
click_button("Create page")
end
expect(current_path).to eq(project_wiki_path(project, "home"))
expect(page).to have_content("test GitLab API doc Rake tasks Wiki header")
.and have_content("Home")
.and have_content("Last edited by #{user.name}")
.and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header")
click_link("test")
page.within('.wiki-form') do expect(current_path).to eq(project_wiki_path(project, "test"))
click_button('Create page')
page.within(:css, ".nav-text") do
expect(page).to have_content("Test").and have_content("Create Page")
end
click_link("Home")
expect(current_path).to eq(project_wiki_path(project, "home"))
click_link("GitLab API")
expect(current_path).to eq(project_wiki_path(project, "api"))
page.within(:css, ".nav-text") do
expect(page).to have_content("Create").and have_content("Api")
end end
expect(page).to have_content('Home') click_link("Home")
expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') expect(current_path).to eq(project_wiki_path(project, "home"))
click_link("Rake tasks")
expect(current_path).to eq(project_wiki_path(project, "raketasks"))
page.within(:css, ".nav-text") do
expect(page).to have_content("Create").and have_content("Rake")
end
end end
it 'creates ASCII wiki with LaTeX blocks', :js do it "creates ASCII wiki with LaTeX blocks", :js do
stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true) stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true)
ascii_content = <<~MD ascii_content = <<~MD
:stem: latexmath :stem: latexmath
...@@ -90,153 +127,164 @@ describe 'User creates wiki page' do ...@@ -90,153 +127,164 @@ describe 'User creates wiki page' do
stem:[2+2] is 4 stem:[2+2] is 4
MD MD
find('#wiki_format option[value=asciidoc]').select_option find("#wiki_format option[value=asciidoc]").select_option
fill_in(:wiki_content, with: ascii_content) fill_in(:wiki_content, with: ascii_content)
page.within('.wiki-form') do page.within(".wiki-form") do
click_button('Create page') click_button("Create page")
end end
page.within '.wiki' do page.within ".wiki" do
expect(page).to have_selector('.katex', count: 3) expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
expect(page).to have_content('2+2 is 4')
end end
end end
end end
context 'in a group namespace', :js do context "in a group namespace", :js do
let(:project) { create(:project, namespace: create(:group, :public)) } let(:project) { create(:project, namespace: create(:group, :public)) }
it 'has "Create home" as a commit message' do it "has `Create home` as a commit message" do
expect(page).to have_field('wiki[message]', with: 'Create home') expect(page).to have_field("wiki[message]", with: "Create home")
end end
it 'creates a page from from the home page' do it "creates a page from from the home page" do
page.within('.wiki-form') do page.within(".wiki-form") do
fill_in(:wiki_content, with: 'My awesome wiki!') fill_in(:wiki_content, with: "My awesome wiki!")
click_button('Create page')
click_button("Create page")
end end
expect(page).to have_content('Home') expect(page).to have_content("Home")
expect(page).to have_content("Last edited by #{user.name}") .and have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') .and have_content("My awesome wiki!")
end end
end end
end end
context 'when wiki is not empty', :js do context "when wiki is not empty", :js do
before do before do
create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' }) create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" })
end end
context 'in a user namespace' do context "in a user namespace" do
let(:project) { create(:project, namespace: user.namespace) } let(:project) { create(:project, namespace: user.namespace) }
context 'via the "new wiki page" page' do context "via the `new wiki page` page" do
it 'creates a page with a single word' do it "creates a page with a single word" do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'foo') fill_in(:new_wiki_path, with: "foo")
click_button('Create page')
click_button("Create page")
end end
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create foo') expect(page).to have_field("wiki[message]", with: "Create foo")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
page.within('.wiki-form') do click_button("Create page")
fill_in(:wiki_content, with: 'My awesome wiki!')
click_button('Create page')
end end
expect(page).to have_content('Foo') expect(page).to have_content("Foo")
expect(page).to have_content("Last edited by #{user.name}") .and have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') .and have_content("My awesome wiki!")
end end
it 'creates a page with spaces in the name' do it "creates a page with spaces in the name" do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'Spaces in the name') fill_in(:new_wiki_path, with: "Spaces in the name")
click_button('Create page')
click_button("Create page")
end end
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create spaces in the name') expect(page).to have_field("wiki[message]", with: "Create spaces in the name")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
page.within('.wiki-form') do click_button("Create page")
fill_in(:wiki_content, with: 'My awesome wiki!')
click_button('Create page')
end end
expect(page).to have_content('Spaces in the name') expect(page).to have_content("Spaces in the name")
expect(page).to have_content("Last edited by #{user.name}") .and have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') .and have_content("My awesome wiki!")
end end
it 'creates a page with hyphens in the name' do it "creates a page with hyphens in the name" do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'hyphens-in-the-name') fill_in(:new_wiki_path, with: "hyphens-in-the-name")
click_button('Create page')
click_button("Create page")
end end
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name') expect(page).to have_field("wiki[message]", with: "Create hyphens in the name")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
page.within('.wiki-form') do click_button("Create page")
fill_in(:wiki_content, with: 'My awesome wiki!')
click_button('Create page')
end end
expect(page).to have_content('Hyphens in the name') expect(page).to have_content("Hyphens in the name")
expect(page).to have_content("Last edited by #{user.name}") .and have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') .and have_content("My awesome wiki!")
end end
end end
it 'shows the autocompletion dropdown' do it "shows the autocompletion dropdown" do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'test-autocomplete') fill_in(:new_wiki_path, with: "test-autocomplete")
click_button('Create page')
click_button("Create page")
end end
page.within('.wiki-form') do page.within(".wiki-form") do
find('#wiki_content').native.send_keys('') find("#wiki_content").native.send_keys("")
fill_in(:wiki_content, with: '@')
fill_in(:wiki_content, with: "@")
end end
expect(page).to have_selector('.atwho-view') expect(page).to have_selector(".atwho-view")
end end
end end
context 'in a group namespace' do context "in a group namespace" do
let(:project) { create(:project, namespace: create(:group, :public)) } let(:project) { create(:project, namespace: create(:group, :public)) }
context 'via the "new wiki page" page' do context "via the `new wiki page` page" do
it 'creates a page' do it "creates a page" do
click_link('New page') click_link("New page")
page.within('#modal-new-wiki') do page.within("#modal-new-wiki") do
fill_in(:new_wiki_path, with: 'foo') fill_in(:new_wiki_path, with: "foo")
click_button('Create page')
click_button("Create page")
end end
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create foo') expect(page).to have_field("wiki[message]", with: "Create foo")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
page.within('.wiki-form') do click_button("Create page")
fill_in(:wiki_content, with: 'My awesome wiki!')
click_button('Create page')
end end
expect(page).to have_content('Foo') expect(page).to have_content("Foo")
expect(page).to have_content("Last edited by #{user.name}") .and have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') .and have_content("My awesome wiki!")
end end
end end
end end
......
require 'spec_helper'
feature 'Active user sessions', :clean_gitlab_redis_shared_state do
scenario 'Successful login adds a new active user login' do
now = Time.zone.parse('2018-03-12 09:06')
Timecop.freeze(now) do
user = create(:user)
gitlab_sign_in(user)
expect(current_path).to eq root_path
sessions = ActiveSession.list(user)
expect(sessions.count).to eq 1
# refresh the current page updates the updated_at
Timecop.freeze(now + 1.minute) do
visit current_path
sessions = ActiveSession.list(user)
expect(sessions.first).to have_attributes(
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:07')
)
end
end
end
scenario 'Successful login cleans up obsolete entries' do
user = create(:user)
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
gitlab_sign_in(user)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
end
end
scenario 'Sessionless login does not clean up obsolete entries' do
user = create(:user)
personal_access_token = create(:personal_access_token, user: user)
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
visit user_path(user, :atom, private_token: personal_access_token.token)
expect(page.status_code).to eq 200
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
end
end
scenario 'Logout deletes the active user login' do
user = create(:user)
gitlab_sign_in(user)
expect(current_path).to eq root_path
expect(ActiveSession.list(user).count).to eq 1
gitlab_sign_out
expect(current_path).to eq new_user_session_path
expect(ActiveSession.list(user)).to be_empty
end
end
import Vue from 'vue'; import Vue from 'vue';
import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip'; import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
const createComponent = () => { const createComponent = () => {
const Component = Vue.extend(wipComponent); const Component = Vue.extend(WorkInProgress);
const mr = { const mr = {
title: 'The best MR ever', title: 'The best MR ever',
removeWIPPath: '/path/to/remove/wip', removeWIPPath: '/path/to/remove/wip',
...@@ -17,10 +17,10 @@ const createComponent = () => { ...@@ -17,10 +17,10 @@ const createComponent = () => {
}); });
}; };
describe('MRWidgetWIP', () => { describe('Wip', () => {
describe('props', () => { describe('props', () => {
it('should have props', () => { it('should have props', () => {
const { mr, service } = wipComponent.props; const { mr, service } = WorkInProgress.props;
expect(mr.type instanceof Object).toBeTruthy(); expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy(); expect(mr.required).toBeTruthy();
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 20180420080616 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
let(:jobs) { table(:ci_builds) }
before do
namespaces.create(id: 10, name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 11, namespace_id: 10, name: 'gitlab', path: 'gitlab')
pipelines.create!(id: 12, project_id: 11, ref: 'master', sha: 'adf43c3a')
stages.create(id: 100, project_id: 11, pipeline_id: 12, name: 'build')
stages.create(id: 101, project_id: 11, pipeline_id: 12, name: 'test')
jobs.create!(id: 121, commit_id: 12, project_id: 11,
stage_idx: 2, stage_id: 100)
jobs.create!(id: 122, commit_id: 12, project_id: 11,
stage_idx: 2, stage_id: 100)
jobs.create!(id: 123, commit_id: 12, project_id: 11,
stage_idx: 10, stage_id: 100)
jobs.create!(id: 124, commit_id: 12, project_id: 11,
stage_idx: 3, stage_id: 101)
end
it 'correctly migrates stages indices' do
expect(stages.all.pluck(:position)).to all(be_nil)
described_class.new.perform(100, 101)
expect(stages.all.pluck(:position)).to eq [2, 3]
end
end
...@@ -17,7 +17,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -17,7 +17,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
context 'when pipeline is ready to be saved' do context 'when pipeline is ready to be saved' do
before do before do
pipeline.stages.build(name: 'test', project: project) pipeline.stages.build(name: 'test', position: 0, project: project)
step.perform! step.perform!
end end
......
...@@ -24,7 +24,8 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do ...@@ -24,7 +24,8 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
describe '#attributes' do describe '#attributes' do
it 'returns hash attributes of a stage' do it 'returns hash attributes of a stage' do
expect(subject.attributes).to be_a Hash expect(subject.attributes).to be_a Hash
expect(subject.attributes).to include(:name, :project) expect(subject.attributes)
.to include(:name, :position, :pipeline, :project)
end end
end end
......
...@@ -232,6 +232,7 @@ Ci::Stage: ...@@ -232,6 +232,7 @@ Ci::Stage:
- id - id
- name - name
- status - status
- position
- lock_version - lock_version
- project_id - project_id
- pipeline_id - pipeline_id
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180420080616_schedule_stages_index_migration')
describe ScheduleStagesIndexMigration, :sidekiq, :migration do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
namespaces.create(id: 12, name: 'gitlab-org', path: 'gitlab-org')
projects.create!(id: 123, namespace_id: 12, name: 'gitlab', path: 'gitlab')
pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
stages.create!(id: 121, project_id: 123, pipeline_id: 1, name: 'build')
stages.create!(id: 122, project_id: 123, pipeline_id: 1, name: 'test')
stages.create!(id: 123, project_id: 123, pipeline_id: 1, name: 'deploy')
end
it 'schedules delayed background migrations in batches' do
Sidekiq::Testing.fake! do
Timecop.freeze do
expect(stages.all).to all(have_attributes(position: be_nil))
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 121, 121)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 122, 122)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 123, 123)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
require 'rails_helper'
RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
let(:user) do
create(:user).tap do |user|
user.current_sign_in_at = Time.current
end
end
let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
let(:request) do
double(:request, {
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
'(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
ip: '127.0.0.1',
session: session
})
end
describe '#current?' do
it 'returns true if the active session matches the current session' do
active_session = ActiveSession.new(session_id: '6919a6f1bb119dd7396fadc38fd18d0d')
expect(active_session.current?(session)).to be true
end
it 'returns false if the active session does not match the current session' do
active_session = ActiveSession.new(session_id: '59822c7d9fcdfa03725eff41782ad97d')
expect(active_session.current?(session)).to be false
end
it 'returns false if the session id is nil' do
active_session = ActiveSession.new(session_id: nil)
session = double(:session, id: nil)
expect(active_session.current?(session)).to be false
end
end
describe '.list' do
it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
end
it 'does not return obsolete entries and cleans them up' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
Gitlab::Redis::SharedState.with do |redis|
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
it 'returns an empty array if the use does not have any active session' do
expect(ActiveSession.list(user)).to eq []
end
end
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each.to_a).to match_array [
"session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
"session:lookup:user:gitlab:#{user.id}"
]
end
end
it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
ActiveSession.set(user, request)
session = ActiveSession.list(user)
expect(session.count).to eq 1
expect(session.first).to have_attributes(
ip_address: '127.0.0.1',
browser: 'Mobile Safari',
os: 'iOS',
device_name: 'iPhone 6',
device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:06'),
session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
)
end
end
it 'keeps the created_at from the login on consecutive requests' do
now = Time.zone.parse('2018-03-12 09:06')
Timecop.freeze(now) do
ActiveSession.set(user, request)
Timecop.freeze(now + 1.minute) do
ActiveSession.set(user, request)
session = ActiveSession.list(user)
expect(session.first).to have_attributes(
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:07')
)
end
end
end
end
describe '.destroy' do
it 'removes the entry associated with the currently killed user session' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '')
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
"session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d",
"session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358"
]
end
end
it 'removes the lookup entry' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
end
end
it 'removes the devise session' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end
end
it 'does not remove the devise session if the active session could not be found' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
end
other_user = create(:user)
ActiveSession.destroy(other_user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty
end
end
end
describe '.cleanup' do
it 'removes obsolete lookup entries' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
it 'does not bail if there are no lookup entries' do
ActiveSession.cleanup(user)
end
end
end
...@@ -51,7 +51,7 @@ describe Ci::Stage, :models do ...@@ -51,7 +51,7 @@ describe Ci::Stage, :models do
end end
end end
describe 'update_status' do describe '#update_status' do
context 'when stage objects needs to be updated' do context 'when stage objects needs to be updated' do
before do before do
create(:ci_build, :success, stage_id: stage.id) create(:ci_build, :success, stage_id: stage.id)
...@@ -87,4 +87,36 @@ describe Ci::Stage, :models do ...@@ -87,4 +87,36 @@ describe Ci::Stage, :models do
end end
end end
end end
describe '#index' do
context 'when stage has been imported and does not have position index set' do
before do
stage.update_column(:position, nil)
end
context 'when stage has statuses' do
before do
create(:ci_build, :running, stage_id: stage.id, stage_idx: 10)
end
it 'recalculates index before updating status' do
expect(stage.reload.position).to be_nil
stage.update_status
expect(stage.reload.position).to eq 10
end
end
context 'when stage does not have statuses' do
it 'fallbacks to zero' do
expect(stage.reload.position).to be_nil
stage.update_status
expect(stage.reload.position).to eq 0
end
end
end
end
end end
...@@ -62,9 +62,7 @@ describe LfsObject do ...@@ -62,9 +62,7 @@ describe LfsObject do
.with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric)) .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
.once .once
lfs_object = create(:lfs_object) create(:lfs_object, :with_file)
lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
lfs_object.save!
end end
end end
end end
......
...@@ -40,7 +40,12 @@ RSpec.describe NotificationSetting do ...@@ -40,7 +40,12 @@ RSpec.describe NotificationSetting do
expect(notification_setting.new_issue).to eq(true) expect(notification_setting.new_issue).to eq(true)
expect(notification_setting.close_issue).to eq(true) expect(notification_setting.close_issue).to eq(true)
expect(notification_setting.merge_merge_request).to eq(true) expect(notification_setting.merge_merge_request).to eq(true)
expect(notification_setting.close_merge_request).to eq(false)
# In Rails 5 assigning a value which is not explicitly `true` or `false` ("nil" in this case)
# to a boolean column transforms it to `true`.
# In Rails 4 it transforms the value to `false` with deprecation warning.
# Replace `eq(Gitlab.rails5?)` with `eq(true)` when removing rails5? code.
expect(notification_setting.close_merge_request).to eq(Gitlab.rails5?)
expect(notification_setting.reopen_merge_request).to eq(false) expect(notification_setting.reopen_merge_request).to eq(false)
end end
end end
......
...@@ -281,7 +281,7 @@ describe API::Jobs do ...@@ -281,7 +281,7 @@ describe API::Jobs do
get_artifact_file(artifact) get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.headers) expect(response.headers.to_h)
.to include('Content-Type' => 'application/json', .to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end end
...@@ -311,7 +311,7 @@ describe API::Jobs do ...@@ -311,7 +311,7 @@ describe API::Jobs do
it 'returns specific job artifacts' do it 'returns specific job artifacts' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.headers).to include(download_headers) expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(job.artifacts_file.file.file) expect(response.body).to match_file(job.artifacts_file.file.file)
end end
end end
...@@ -462,7 +462,7 @@ describe API::Jobs do ...@@ -462,7 +462,7 @@ describe API::Jobs do
end end
it { expect(response).to have_http_status(:ok) } it { expect(response).to have_http_status(:ok) }
it { expect(response.headers).to include(download_headers) } it { expect(response.headers.to_h).to include(download_headers) }
end end
context 'when artifacts are stored remotely' do context 'when artifacts are stored remotely' do
......
...@@ -1355,7 +1355,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1355,7 +1355,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it 'download artifacts' do it 'download artifacts' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.headers).to include download_headers expect(response.headers.to_h).to include download_headers
end end
end end
...@@ -1370,7 +1370,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1370,7 +1370,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it 'uses workhorse send-url' do it 'uses workhorse send-url' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.headers).to include( expect(response.headers.to_h).to include(
'Gitlab-Workhorse-Send-Data' => /send-url:/) 'Gitlab-Workhorse-Send-Data' => /send-url:/)
end end
end end
......
...@@ -232,7 +232,7 @@ describe API::V3::Builds do ...@@ -232,7 +232,7 @@ describe API::V3::Builds do
it 'returns specific job artifacts' do it 'returns specific job artifacts' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers) expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file) expect(response.body).to match_file(build.artifacts_file.file.file)
end end
end end
...@@ -332,7 +332,7 @@ describe API::V3::Builds do ...@@ -332,7 +332,7 @@ describe API::V3::Builds do
end end
it { expect(response).to have_http_status(200) } it { expect(response).to have_http_status(200) }
it { expect(response.headers).to include(download_headers) } it { expect(response.headers.to_h).to include(download_headers) }
end end
context 'when artifacts are stored remotely' do context 'when artifacts are stored remotely' do
......
require 'spec_helper' require "spec_helper"
describe ::Applications::CreateService do describe ::Applications::CreateService do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:params) { attributes_for(:application) } let(:params) { attributes_for(:application) }
let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') } let(:request) do
if Gitlab.rails5?
ActionController::TestRequest.new({ remote_ip: "127.0.0.1" }, ActionController::TestSession.new)
else
ActionController::TestRequest.new(remote_ip: "127.0.0.1")
end
end
subject { described_class.new(user, params) } subject { described_class.new(user, params) }
it 'creates an application' do it { expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1) }
expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
end
end end
...@@ -6,7 +6,9 @@ describe Ci::RetryBuildService do ...@@ -6,7 +6,9 @@ describe Ci::RetryBuildService do
set(:pipeline) { create(:ci_pipeline, project: project) } set(:pipeline) { create(:ci_pipeline, project: project) }
let(:stage) do let(:stage) do
Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') create(:ci_stage_entity, project: project,
pipeline: pipeline,
name: 'test')
end end
let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) } let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) }
......
...@@ -219,7 +219,7 @@ describe MergeRequests::MergeService do ...@@ -219,7 +219,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request) service.execute(merge_request)
expect(merge_request.merge_error).to include(error_message) expect(merge_request.merge_error).to include('Something went wrong during merge')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
...@@ -231,7 +231,7 @@ describe MergeRequests::MergeService do ...@@ -231,7 +231,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request) service.execute(merge_request)
expect(merge_request.merge_error).to include(error_message) expect(merge_request.merge_error).to include('Something went wrong during merge pre-receive hook')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
......
...@@ -123,11 +123,13 @@ describe Projects::UpdatePagesService do ...@@ -123,11 +123,13 @@ describe Projects::UpdatePagesService do
expect(execute).not_to eq(:success) expect(execute).not_to eq(:success)
end end
it 'fails for empty file fails' do context 'when using empty file' do
build.job_artifacts_archive.update_attributes(file: empty_file) let(:file) { empty_file }
expect { execute } it 'fails to extract' do
.to raise_error(Projects::UpdatePagesService::FailedToExtractError) expect { execute }
.to raise_error(Projects::UpdatePagesService::FailedToExtractError)
end
end end
context 'when timeout happens by DNS error' do context 'when timeout happens by DNS error' do
......
...@@ -79,7 +79,7 @@ RSpec.shared_examples 'a creatable merge request' do ...@@ -79,7 +79,7 @@ RSpec.shared_examples 'a creatable merge request' do
end end
end end
it 'updates the branches when selecting a new target project' do it 'updates the branches when selecting a new target project', :js do
target_project_member = target_project.owner target_project_member = target_project.owner
CreateBranchService.new(target_project, target_project_member) CreateBranchService.new(target_project, target_project_member)
.execute('a-brand-new-branch-to-test', 'master') .execute('a-brand-new-branch-to-test', 'master')
...@@ -92,7 +92,7 @@ RSpec.shared_examples 'a creatable merge request' do ...@@ -92,7 +92,7 @@ RSpec.shared_examples 'a creatable merge request' do
first('.js-target-branch').click first('.js-target-branch').click
within('.dropdown-target-branch .dropdown-content') do within('.js-target-branch-dropdown .dropdown-content') do
expect(page).to have_content('a-brand-new-branch-to-test') expect(page).to have_content('a-brand-new-branch-to-test')
end end
end end
......
...@@ -46,8 +46,7 @@ describe LfsObjectUploader do ...@@ -46,8 +46,7 @@ describe LfsObjectUploader do
end end
describe 'remote file' do describe 'remote file' do
let(:remote) { described_class::Store::REMOTE } let(:lfs_object) { create(:lfs_object, :object_storage, :with_file) }
let(:lfs_object) { create(:lfs_object, file_store: remote) }
context 'with object storage enabled' do context 'with object storage enabled' do
before do before do
...@@ -57,16 +56,11 @@ describe LfsObjectUploader do ...@@ -57,16 +56,11 @@ describe LfsObjectUploader do
it 'can store file remotely' do it 'can store file remotely' do
allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async) allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
store_file(lfs_object) lfs_object
expect(lfs_object.file_store).to eq remote expect(lfs_object.file_store).to eq(described_class::Store::REMOTE)
expect(lfs_object.file.path).not_to be_blank expect(lfs_object.file.path).not_to be_blank
end end
end end
end end
def store_file(lfs_object)
lfs_object.file = fixture_file_upload(Rails.root.join("spec/fixtures/dk.png"), "`/png")
lfs_object.save!
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment