Commit 7b8f121e authored by Nick Thomas's avatar Nick Thomas

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes #2175, gitlab-ce#30810, gitlab-ce#27655, and gitlab-ce#30863

See merge request !1664
parents a0823c87 76e772e7
...@@ -72,6 +72,7 @@ stages: ...@@ -72,6 +72,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
...@@ -92,6 +93,7 @@ stages: ...@@ -92,6 +93,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
......
...@@ -57,16 +57,16 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -57,16 +57,16 @@ star, smile, etc.). Some good tips about code reviews can be found in our
[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html [Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze ## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period, Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th ### Between the 1st and the 7th
These types of merge requests need special consideration: These types of merge requests for the upcoming release need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off * **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack and the release blogpost; typically this will have its own channel in Slack
...@@ -114,14 +114,15 @@ subsequent EE merge, as we often merge a lot to CE on the release date. For more ...@@ -114,14 +114,15 @@ subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts]. [limit conflicts with EE when developing on CE][limit_ee_conflicts].
### Between the 7th and the 22nd ### After the 7th
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd). These fixes will be shipped in the next RC for that release if it is before the 22nd.
If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
If you think a merge request should go into the upcoming release even though it does not meet these requirements, If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer: you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
1. a Release Manager 1. a Release Manager
......
...@@ -8,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji'; ...@@ -8,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
const requestAnimationFrame = window.requestAnimationFrame || const requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
...@@ -105,8 +106,9 @@ function AwardsHandler() { ...@@ -105,8 +106,9 @@ function AwardsHandler() {
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon'); const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current'); $target.closest('.js-awards-block').addClass('current');
return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji);
}); });
} }
...@@ -132,12 +134,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -132,12 +134,12 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
if ($menu.is('.is-visible')) { if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active'); $addBtn.removeClass('is-active');
$menu.removeClass('is-visible'); $menu.removeClass('is-visible');
$('#emoji_search').blur(); $('.js-emoji-menu-search').blur();
} else { } else {
$addBtn.addClass('is-active'); $addBtn.addClass('is-active');
this.positionMenu($menu, $addBtn); this.positionMenu($menu, $addBtn);
$menu.addClass('is-visible'); $menu.addClass('is-visible');
$('#emoji_search').focus(); $('.js-emoji-menu-search').focus();
} }
} else { } else {
$addBtn.addClass('is-loading is-active'); $addBtn.addClass('is-loading is-active');
...@@ -147,7 +149,7 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -147,7 +149,7 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
this.positionMenu($createdMenu, $addBtn); this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => { return setTimeout(() => {
$createdMenu.addClass('is-visible'); $createdMenu.addClass('is-visible');
$('#emoji_search').focus(); $('.js-emoji-menu-search').focus();
}, 200); }, 200);
}); });
} }
...@@ -180,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { ...@@ -180,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
const emojiMenuMarkup = ` const emojiMenuMarkup = `
<div class="emoji-menu"> <div class="emoji-menu">
<input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content"> <div class="emoji-menu-content">
${frequentlyUsedCatgegory} ${frequentlyUsedCatgegory}
...@@ -500,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj ...@@ -500,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
}; };
AwardsHandler.prototype.setupSearch = function setupSearch() { AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { const $search = $('.js-emoji-menu-search');
this.registerEventListener('on', $search, 'input', (e) => {
const term = $(e.target).val().trim(); const term = $(e.target).val().trim();
// Clean previous search results this.searchEmojis(term);
$('ul.emoji-menu-search, h5.emoji-search-title').remove(); });
if (term.length > 0) {
// Generate a search result block const $menu = $('.emoji-menu');
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
const foundEmojis = this.searchEmojis(term).show(); if (e.target === e.currentTarget) {
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); // Clear the search
$('.emoji-menu-content ul, .emoji-menu-content h5').hide(); this.searchEmojis('');
$('.emoji-menu-content').append(h5).append(ul);
} else {
$('.emoji-menu-content').children().show();
} }
}); });
}; };
AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { AwardsHandler.prototype.searchEmojis = function searchEmojis(term) {
const $search = $('.js-emoji-menu-search');
$search.val(term);
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
$('.emoji-menu-content').append(h5).append(ul);
} else {
$('.emoji-menu-content').children().show();
}
};
AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase(); const safeTerm = term.toLowerCase();
const namesMatchingAlias = []; const namesMatchingAlias = [];
......
...@@ -22,6 +22,7 @@ $(() => { ...@@ -22,6 +22,7 @@ $(() => {
} }
$('body').on('click', '.js-toggle-button', function toggleButton(e) { $('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container')); toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase(); const targetTag = e.currentTarget.tagName.toLowerCase();
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
/* global ListLabel */ /* global ListLabel */
import queryData from '../utils/query_data'; import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List { class List {
constructor (obj) { constructor (obj) {
this.id = obj.id; this.id = obj.id;
...@@ -58,7 +60,9 @@ class List { ...@@ -58,7 +60,9 @@ class List {
nextPage () { nextPage () {
if (this.issuesSize > this.issues.length) { if (this.issuesSize > this.issues.length) {
this.page += 1; if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
}
return this.getIssues(false); return this.getIssues(false);
} }
...@@ -146,10 +150,7 @@ class List { ...@@ -146,10 +150,7 @@ class List {
} }
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
.then(() => {
listFrom.getIssues(false);
});
} }
findIssue (id) { findIssue (id) {
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
consistent-return, prefer-rest-params */ consistent-return, prefer-rest-params */
/* global Breakpoints */ /* global Breakpoints */
import { bytesToKiB } from './lib/utils/number_utils';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; }; const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75; const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace'; const DOWN_BUILD_TRACE = '#down-build-trace';
...@@ -20,6 +22,7 @@ window.Build = (function () { ...@@ -20,6 +22,7 @@ window.Build = (function () {
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.logBytes = 0;
this.updateDropdown = bind(this.updateDropdown, this); this.updateDropdown = bind(this.updateDropdown, this);
...@@ -98,15 +101,22 @@ window.Build = (function () { ...@@ -98,15 +101,22 @@ window.Build = (function () {
if (log.append) { if (log.append) {
$buildContainer.append(log.html); $buildContainer.append(log.html);
this.logBytes += log.size;
} else { } else {
$buildContainer.html(log.html); $buildContainer.html(log.html);
if (log.truncated) { this.logBytes = log.size;
$('.js-truncated-info-size').html(` ${log.size} `); }
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo(); // if the incremental sum of logBytes we received is less than the total
} else { // we need to show a message warning the user about that.
this.$truncatedInfo.addClass('hidden'); if (this.logBytes < log.total) {
} // size is in bytes, we need to calculate KiB
const size = bytesToKiB(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
} }
this.checkAutoscroll(); this.checkAutoscroll();
......
...@@ -125,7 +125,7 @@ $(() => { ...@@ -125,7 +125,7 @@ $(() => {
}, },
dismissOverviewDialog() { dismissOverviewDialog() {
this.isOverviewDialogDismissed = true; this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
}, },
}, },
}); });
......
...@@ -20,7 +20,6 @@ const ResolveBtn = Vue.extend({ ...@@ -20,7 +20,6 @@ const ResolveBtn = Vue.extend({
return { return {
discussions: CommentsStore.state, discussions: CommentsStore.state,
loading: false, loading: false,
note: {},
}; };
}, },
watch: { watch: {
...@@ -33,6 +32,9 @@ const ResolveBtn = Vue.extend({ ...@@ -33,6 +32,9 @@ const ResolveBtn = Vue.extend({
discussion: function () { discussion: function () {
return this.discussions[this.discussionId]; return this.discussions[this.discussionId];
}, },
note: function () {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
buttonText: function () { buttonText: function () {
if (this.isResolved) { if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`; return `Resolved by ${this.resolvedByName}`;
...@@ -111,8 +113,6 @@ const ResolveBtn = Vue.extend({ ...@@ -111,8 +113,6 @@ const ResolveBtn = Vue.extend({
authorAvatar: this.authorAvatar, authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated, noteTruncated: this.noteTruncated,
}); });
this.note = this.discussion.getNote(this.noteId);
} }
}); });
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
/* global Sidebar */ /* global Sidebar */
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
/* global ShortcutsWiki */
import Issue from './issue'; import Issue from './issue';
...@@ -49,6 +50,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; ...@@ -49,6 +50,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import GeoNodes from './geo_nodes'; import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root'; import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
...@@ -446,7 +448,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -446,7 +448,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'wikis': case 'wikis':
new gl.Wikis(); new gl.Wikis();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form')); new gl.GLForm($('.wiki-form'));
break; break;
......
...@@ -66,7 +66,10 @@ window.DropzoneInput = (function() { ...@@ -66,7 +66,10 @@ window.DropzoneInput = (function() {
form_textarea.focus(); form_textarea.focus();
}, },
success: function(header, response) { success: function(header, response) {
pasteText(response.link.markdown); const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
pasteText(response.link.markdown, shouldPad);
}, },
error: function(temp) { error: function(temp) {
var checkIfMsgExists, errorAlert; var checkIfMsgExists, errorAlert;
...@@ -123,12 +126,15 @@ window.DropzoneInput = (function() { ...@@ -123,12 +126,15 @@ window.DropzoneInput = (function() {
} }
return false; return false;
}; };
pasteText = function(text) { pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text + "\n\n"; var formattedText = text;
if (shouldPad) formattedText += "\n\n";
const textarea = child.get(0); const textarea = child.get(0);
caretStart = textarea.selectionStart; caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd; caretEnd = textarea.selectionEnd;
caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length; textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart); beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd); afterSelection = $(child).val().substring(caretEnd, textEnd);
......
import Vue from 'vue'; import Vue from 'vue';
import IssueTitle from './issue_title'; import IssueTitle from './issue_title.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
const vueOptions = () => ({ (() => {
el: '.issue-title-entrypoint', const issueTitleData = document.querySelector('.issue-title-data').dataset;
components: { const { initialTitle, endpoint } = issueTitleData;
IssueTitle,
},
data() {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
return { const vm = new Vue({
initialTitle: issueTitleData.initialTitle, el: '.issue-title-entrypoint',
endpoint: issueTitleData.endpoint, render: createElement => createElement(IssueTitle, {
}; props: {
}, initialTitle,
template: ` endpoint,
<IssueTitle },
:initialTitle="initialTitle" }),
:endpoint="endpoint" });
/>
`,
});
(() => new Vue(vueOptions()))(); return vm;
})();
<script>
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll'; import Poll from './../lib/utils/poll';
import Service from './services/index'; import Service from './services/index';
...@@ -72,7 +73,9 @@ export default { ...@@ -72,7 +73,9 @@ export default {
created() { created() {
this.fetch(); this.fetch();
}, },
template: `
<h2 class='title' v-html='title'></h2>
`,
}; };
</script>
<template>
<h2 class="title" v-html="title"></h2>
</template>
/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
/* eslint-disable import/prefer-default-export */ import { BYTES_IN_KIB } from './constants';
/** /**
* Function that allows a number with an X amount of decimals * Function that allows a number with an X amount of decimals
...@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) { ...@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) {
} }
return formattedNumber; return formattedNumber;
} }
/**
* Utility function that calculates KiB of the given bytes.
*
* @param {Number} number bytes
* @return {Number} KiB
*/
export function bytesToKiB(number) {
return number / BYTES_IN_KIB;
}
/* eslint-disable class-methods-use-this */
/* global Mousetrap */
/* global ShortcutsNavigation */
import findAndFollowLink from './shortcuts_dashboard_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
Mousetrap.bind('e', this.editWiki);
}
editWiki() {
findAndFollowLink('.js-wiki-edit');
}
}
...@@ -18,7 +18,7 @@ export default class UserCallout { ...@@ -18,7 +18,7 @@ export default class UserCallout {
dismissCallout(e) { dismissCallout(e) {
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
Cookies.set(USER_CALLOUT_COOKIE, 'true'); Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) { if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
......
...@@ -155,7 +155,7 @@ header { ...@@ -155,7 +155,7 @@ header {
.header-logo { .header-logo {
display: inline-block; display: inline-block;
margin: 0 7px 0 2px; margin: 0 12px 0 2px;
position: relative; position: relative;
top: 10px; top: 10px;
transition-duration: .3s; transition-duration: .3s;
...@@ -186,7 +186,7 @@ header { ...@@ -186,7 +186,7 @@ header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
flex: 1 1 auto; flex: 1 1 auto;
padding-top: (($header-height - 19) / 2); padding-top: 14px;
overflow: hidden; overflow: hidden;
} }
......
...@@ -61,8 +61,9 @@ ...@@ -61,8 +61,9 @@
.truncated-info { .truncated-info {
text-align: center; text-align: center;
border-bottom: 1px solid; border-bottom: 1px solid;
background-color: $black-transparent; background-color: $black;
height: 45px; height: 45px;
padding: 15px;
&.affix { &.affix {
top: 0; top: 0;
...@@ -87,6 +88,16 @@ ...@@ -87,6 +88,16 @@
right: 5px; right: 5px;
left: 5px; left: 5px;
} }
.truncated-info-size {
margin: 0 5px;
}
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
} }
} }
......
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
.text-expander { .text-expander {
display: inline-block; display: inline-block;
background: $gray-light; background: $white-light;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
padding: 0 5px; padding: 0 5px;
cursor: pointer; cursor: pointer;
...@@ -146,6 +146,11 @@ ...@@ -146,6 +146,11 @@
line-height: $gl-font-size; line-height: $gl-font-size;
outline: none; outline: none;
&.open {
background: $gray-light;
box-shadow: inset 0 0 2px rgba($black, 0.2);
}
&:hover { &:hover {
background-color: darken($gray-light, 10%); background-color: darken($gray-light, 10%);
text-decoration: none; text-decoration: none;
......
...@@ -106,6 +106,10 @@ ...@@ -106,6 +106,10 @@
span { span {
white-space: pre-wrap; white-space: pre-wrap;
} }
.line {
word-wrap: break-word;
}
} }
} }
......
...@@ -123,6 +123,9 @@ ul.notes { ...@@ -123,6 +123,9 @@ ul.notes {
} }
.note-emoji-button { .note-emoji-button {
position: relative;
line-height: 1;
.fa-spinner { .fa-spinner {
display: none; display: none;
} }
...@@ -352,6 +355,15 @@ ul.notes { ...@@ -352,6 +355,15 @@ ul.notes {
font-size: 14px; font-size: 14px;
} }
.note-header {
display: flex;
justify-content: space-between;
}
.note-header-info {
min-width: 0;
}
.note-headline-light { .note-headline-light {
display: inline; display: inline;
...@@ -371,21 +383,27 @@ ul.notes { ...@@ -371,21 +383,27 @@ ul.notes {
} }
} }
.note-headline-meta {
display: inline-block;
white-space: nowrap;
}
/** /**
* Actions for Discussions/Notes * Actions for Discussions/Notes
*/ */
.discussion-actions, .discussion-actions {
.note-actions {
float: right; float: right;
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
} }
.note-actions { .note-actions {
position: absolute; flex-shrink: 0;
right: 0; // For PhantomJS that does not support flex
top: 0; float: right;
margin-left: 10px;
color: $gray-darkest;
.note-action-button { .note-action-button {
margin-left: 8px; margin-left: 8px;
...@@ -428,7 +446,8 @@ ul.notes { ...@@ -428,7 +446,8 @@ ul.notes {
.award-control-icon-positive, .award-control-icon-positive,
.award-control-icon-super-positive { .award-control-icon-super-positive {
position: absolute; position: absolute;
margin-left: -20px; top: 0;
left: 0;
opacity: 0; opacity: 0;
} }
......
...@@ -970,27 +970,23 @@ a.allowed-to-push { ...@@ -970,27 +970,23 @@ a.allowed-to-push {
} }
.variable-key { .variable-key {
width: 300px; max-width: 120px;
max-width: 300px;
overflow: hidden; overflow: hidden;
word-wrap: break-word; word-wrap: break-word;
white-space: nowrap;
// override bootstrap text-overflow: ellipsis;
white-space: normal!important;
@media (max-width: $screen-sm-max) {
width: 150px;
max-width: 150px;
}
} }
.variable-value { .variable-value {
@media(max-width: $screen-xs-max) { max-width: 150px;
width: 150px; overflow: hidden;
max-width: 150px; word-wrap: break-word;
overflow: hidden; white-space: nowrap;
word-wrap: break-word; text-overflow: ellipsis;
} }
.variable-menu {
text-align: right;
} }
} }
......
...@@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController ...@@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
spam_log = SpamLog.find(params[:id]) spam_log = SpamLog.find(params[:id])
if params[:remove_user] if params[:remove_user]
spam_log.remove_user spam_log.remove_user(deleted_by: current_user)
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
else else
spam_log.destroy spam_log.destroy
......
...@@ -34,7 +34,7 @@ module BlobHelper ...@@ -34,7 +34,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly # This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_edit_blob?(blob, project, ref)) elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project) elsif current_user && can?(current_user, :fork_project, project)
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler" button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
...@@ -52,7 +52,7 @@ module BlobHelper ...@@ -52,7 +52,7 @@ module BlobHelper
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer? elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project) elsif can?(current_user, :fork_project, project)
continue_params = { continue_params = {
...@@ -90,7 +90,7 @@ module BlobHelper ...@@ -90,7 +90,7 @@ module BlobHelper
) )
end end
def can_edit_blob?(blob, project = @project, ref = @ref) def can_modify_blob?(blob, project = @project, ref = @ref)
!blob.lfs_pointer? && can_edit_tree?(project, ref) !blob.lfs_pointer? && can_edit_tree?(project, ref)
end end
......
...@@ -24,7 +24,7 @@ module ProjectsHelper ...@@ -24,7 +24,7 @@ module ProjectsHelper
return "(deleted)" unless author return "(deleted)" unless author
author_html = "" author_html = ""
# Build avatar image tag # Build avatar image tag
author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar] author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
...@@ -45,7 +45,7 @@ module ProjectsHelper ...@@ -45,7 +45,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else else
title = opts[:title].sub(":name", sanitize(author.name)) title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe
end end
end end
...@@ -457,13 +457,22 @@ module ProjectsHelper ...@@ -457,13 +457,22 @@ module ProjectsHelper
end end
def visibility_select_options(project, selected_level) def visibility_select_options(project, selected_level)
levels_options_array = Gitlab::VisibilityLevel.values.map do |level| level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options|
[ next if restricted_levels.include?(level)
level_options << [
visibility_level_label(level), visibility_level_label(level),
{ data: { description: visibility_level_description(level, project) } }, { data: { description: visibility_level_description(level, project) } },
level level
] ]
end end
options_for_select(levels_options_array, selected_level)
options_for_select(level_options, selected_level)
end
def restricted_levels
return [] if current_user.admin?
current_application_settings.restricted_visibility_levels || []
end end
end end
...@@ -42,7 +42,7 @@ module SnippetsHelper ...@@ -42,7 +42,7 @@ module SnippetsHelper
0, 0,
lined_content.size, lined_content.size,
surrounding_lines surrounding_lines
) if line.include?(query) ) if line.downcase.include?(query.downcase)
end end
used_lines.uniq.sort used_lines.uniq.sort
......
...@@ -16,7 +16,7 @@ class AbuseReport < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class AbuseReport < ActiveRecord::Base
def remove_user(deleted_by:) def remove_user(deleted_by:)
user.block user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end end
def notify def notify
......
...@@ -26,7 +26,7 @@ class Repository ...@@ -26,7 +26,7 @@ class Repository
# #
# For example, for entry `:readme` there's a method called `readme` which # For example, for entry `:readme` there's a method called `readme` which
# stores its data in the `readme` cache key. # stores its data in the `readme` cache key.
CACHED_METHODS = %i(size commit_count readme version contribution_guide CACHED_METHODS = %i(size commit_count readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze tag_count avatar exists? empty? root_ref).freeze
...@@ -39,7 +39,6 @@ class Repository ...@@ -39,7 +39,6 @@ class Repository
changelog: :changelog, changelog: :changelog,
license: %i(license_blob license_key), license: %i(license_blob license_key),
contributing: :contribution_guide, contributing: :contribution_guide,
version: :version,
gitignore: :gitignore, gitignore: :gitignore,
koding: :koding_yml, koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml, gitlab_ci: :gitlab_ci_yml,
...@@ -116,7 +115,7 @@ class Repository ...@@ -116,7 +115,7 @@ class Repository
offset: offset, offset: offset,
after: after, after: after,
before: before, before: before,
follow: path.present?, follow: Array(path).length == 1,
skip_merges: skip_merges skip_merges: skip_merges
} }
...@@ -537,11 +536,6 @@ class Repository ...@@ -537,11 +536,6 @@ class Repository
end end
cache_method :readme cache_method :readme
def version
file_on_head(:version)
end
cache_method :version
def contribution_guide def contribution_guide
file_on_head(:contributing) file_on_head(:contributing)
end end
......
...@@ -3,9 +3,9 @@ class SpamLog < ActiveRecord::Base ...@@ -3,9 +3,9 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true validates :user, presence: true
def remove_user def remove_user(deleted_by:)
user.block user.block
user.destroy DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end end
def text def text
......
...@@ -59,7 +59,6 @@ module Projects ...@@ -59,7 +59,6 @@ module Projects
project.repository.add_remote(project.import_type, project.import_url) project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type) project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true) project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
end end
def import_data def import_data
......
...@@ -26,7 +26,7 @@ module Users ...@@ -26,7 +26,7 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end end
MigrateToGhostUserService.new(user).execute MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace namespace = user.namespace
......
...@@ -318,3 +318,11 @@ ...@@ -318,3 +318,11 @@
%td.shortcut %td.shortcut
.key l .key l
%td Change Label %td Change Label
%tbody.hidden-shortcut.wiki{ style: 'display:none' }
%tr
%th
%th Wiki pages
%tr
%td.shortcut
.key e
%td Edit wiki page
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
#tree-holder.tree-holder #tree-holder.tree-holder
= render 'blob', blob: @blob = render 'blob', blob: @blob
- if can_edit_blob?(@blob) - if can_modify_blob?(@blob)
= render 'projects/blob/remove' = render 'projects/blob/remove'
- title = "Replace #{@blob.name}" - title = "Replace #{@blob.name}"
......
...@@ -73,11 +73,11 @@ ...@@ -73,11 +73,11 @@
= custom_icon('scroll_down_hover_active') = custom_icon('scroll_down_hover_active')
#up-build-trace #up-build-trace
%pre.build-trace#build-trace %pre.build-trace#build-trace
.js-truncated-info.truncated-info.hidden .js-truncated-info.truncated-info.hidden<
%span< Showing last
Showing last %span.js-truncated-info-size.truncated-info-size><
%span.js-truncated-info-size>< KiB of log -
KiB of log %a.js-raw-link.raw-link{ :href => raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
%code.bash.js-build-output %code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if diff_file.too_large? - if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large. .nothing-here-block This diff could not be displayed because it is too large.
- elsif blob.only_display_raw? - elsif blob.only_display_raw?
.nothing-here-block This file is too large to display. .nothing-here-block The file could not be displayed because it is too large.
- elsif blob_text_viewable?(blob) - elsif blob_text_viewable?(blob)
- if !project.repository.diffable?(blob) - if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry. .nothing-here-block This diff was suppressed by a .gitattributes entry.
......
...@@ -79,4 +79,5 @@ ...@@ -79,4 +79,5 @@
= render 'shared/issuable/sidebar', issuable: @issue = render 'shared/issuable/sidebar', issuable: @issue
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issue_show') = page_specific_javascript_bundle_tag('issue_show')
...@@ -12,19 +12,21 @@ ...@@ -12,19 +12,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content .timeline-content
.note-header .note-header
%a.visible-xs{ href: user_path(note.author) } .note-header-info
= note.author.to_reference %a{ href: user_path(note.author) }
= link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs') %span.hidden-xs
.note-headline-light = sanitize(note.author.name)
%span.hidden-xs %span.note-headline-light
= note.author.to_reference = note.author.to_reference
- unless note.system %span.note-headline-light
commented %span.note-headline-meta
- if note.system - unless note.system
%span.system-note-message commented
= note.redacted_note_html - if note.system
%a{ href: "##{dom_id(note)}" } %span.system-note-message
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') = note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system? - unless note.system?
.note-actions .note-actions
- access = note_max_access_for_user(note) - access = note_max_access_for_user(note)
......
- page_title "Edit", @snippet.title, "Snippets" - page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title %h3.page-title
Edit Snippet Edit Snippet
......
- page_title @snippet.title, "Snippets" - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header' = render 'shared/snippets/header'
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%tr %tr
%td.variable-key= variable.key %td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }****** %td.variable-value{ "data-value" => variable.value }******
%td %td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only %span.sr-only
Update Update
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page history Page history
- if can?(current_user, :create_wiki, @project) && @page.latest? - if can?(current_user, :create_wiki, @project) && @page.latest?
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn js-wiki-edit" do
Edit Edit
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
= confidential_icon(issue) = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title %span.term.str-truncated= issue.title
- if issue.closed?
%span.label.label-danger.prepend-left-5 Closed
.pull-right ##{issue.iid} .pull-right ##{issue.iid}
- if issue.description.present? - if issue.description.present?
.description.term .description.term
...@@ -10,6 +12,3 @@ ...@@ -10,6 +12,3 @@
= search_md_sanitize(issue, :description) = search_md_sanitize(issue, :description)
%span.light %span.light
#{issue.project.name_with_namespace} #{issue.project.name_with_namespace}
- if issue.closed?
.pull-right
%span.label.label-danger Closed
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
%h4 %h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title %span.term.str-truncated= merge_request.title
- if merge_request.merged?
%span.label.label-primary.prepend-left-5 Merged
- elsif merge_request.closed?
%span.label.label-danger.prepend-left-5 Closed
.pull-right= merge_request.to_reference .pull-right= merge_request.to_reference
- if merge_request.description.present? - if merge_request.description.present?
.description.term .description.term
...@@ -9,8 +13,3 @@ ...@@ -9,8 +13,3 @@
= search_md_sanitize(merge_request, :description) = search_md_sanitize(merge_request, :description)
%span.light %span.light
#{merge_request.project.name_with_namespace} #{merge_request.project.name_with_namespace}
.pull-right
- if merge_request.merged?
%span.label.label-primary Merged
- elsif merge_request.closed?
%span.label.label-danger Closed
...@@ -16,13 +16,14 @@ ...@@ -16,13 +16,14 @@
class: "check_all_issues left" class: "check_all_issues left"
.issues-other-filters.filtered-search-wrapper .issues-other-filters.filtered-search-wrapper
.filtered-search-box .filtered-search-box
= dropdown_tag(content_tag(:i, '', class: 'fa fa-history'), - if type != :boards_modal && type != :boards
options: { wrapper_class: "filtered-search-history-dropdown-wrapper", = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
toggle_class: "filtered-search-history-dropdown-toggle-button", options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
dropdown_class: "filtered-search-history-dropdown", toggle_class: "filtered-search-history-dropdown-toggle-button",
content_class: "filtered-search-history-dropdown-content", dropdown_class: "filtered-search-history-dropdown",
title: "Recent searches" }) do content_class: "filtered-search-history-dropdown-content",
.js-filtered-search-history-dropdown title: "Recent searches" }) do
.js-filtered-search-history-dropdown
.filtered-search-box-input-container .filtered-search-box-input-container
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
......
- page_title "Edit", @snippet.title, "Snippets" - page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title %h3.page-title
Edit Snippet Edit Snippet
%hr %hr
......
- page_title @snippet.title, "Snippets" - page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header' = render 'shared/snippets/header'
......
---
title: Clear emoji search in awards menu after picking emoji
merge_request:
author:
---
title: Expand/collapse button -> Change to make it look like a toggle
merge_request: 10720
author: Jacopo Beschi @jacopo-beschi
---
title: Add keyboard edit shotcut for wiki
merge_request: 10245
author: George Andrinopoulos
---
title: Remove Repository#version method and tests
merge_request: 10734
author:
---
title: Fix trace cannot be written due to encoding
merge_request: 10758
author:
---
title: Fix restricted project visibility setting available to users
merge_request:
author:
---
title: Fix another case where trace does not have proper encoding set
merge_request: 10728
author:
---
title: Move labels of search results from bottom to title
merge_request: 10705
author: dr
---
title: Only add newlines between multiple uploads
merge_request: 10545
author:
...@@ -8,7 +8,12 @@ Rails.application.configure do ...@@ -8,7 +8,12 @@ Rails.application.configure do
# test suite. You never need to work with it otherwise. Remember that # test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped # your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there! # and recreated between test runs. Don't rely on the data there!
config.cache_classes = false
# Enabling caching of classes slows start-up time because all controllers
# are loaded at initalization, but it reduces memory and load because files
# are not reloaded with every request. For example, caching is not necessary
# for loading database migrations but useful for handling Knapsack specs.
config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.assets.digest = false config.assets.digest = false
......
...@@ -42,29 +42,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -42,29 +42,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ } resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
end end
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
member do
get :charts
get :commits
get :ci
get :languages
end
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do member do
get 'raw' get 'raw'
...@@ -142,12 +119,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -142,12 +119,6 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
## EE-specific ## EE-specific
resources :path_locks, only: [:index, :destroy] do resources :path_locks, only: [:index, :destroy] do
collection do collection do
...@@ -159,16 +130,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -159,16 +130,6 @@ constraints(ProjectUrlConstrainer.new) do
get '/service_desk' => 'service_desk#show', as: :service_desk get '/service_desk' => 'service_desk#show', as: :service_desk
put '/service_desk' => 'service_desk#update', as: :service_desk_refresh put '/service_desk' => 'service_desk#update', as: :service_desk_refresh
resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::Regex.git_reference_regex } do
## EE-specific
scope module: :protected_branches do
resources :merge_access_levels, only: [:destroy]
resources :push_access_levels, only: [:destroy]
end
end
resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :edit, :update, :destroy] do resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do member do
......
# All routing related to repositoty browsing # All routing related to repository browsing
resource :repository, only: [:create] do resource :repository, only: [:create] do
member do member do
...@@ -6,83 +6,91 @@ resource :repository, only: [:create] do ...@@ -6,83 +6,91 @@ resource :repository, only: [:create] do
end end
end end
resources :refs, only: [] do # Don't use format parameter as file extension (old 3.0.x behavior)
collection do # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
get 'switch' scope format: false do
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end end
member do resources :refs, only: [] do
# tree viewer logs collection do
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } get 'switch'
# Directories with leading dots erroneously get rejected if git end
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { member do
id: /.*/, # tree viewer logs
path: /.*/ get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
} # Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
id: /.*/,
path: /.*/
}
end
end end
end
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' scope constraints: { id: Gitlab::Regex.git_reference_regex } do
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' resources :network, only: [:show]
get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
scope('/blob/*id', as: :blob, controller: :blob, constraints: { id: /.+/, format: false }) do
get :diff
get '/', action: :show
delete '/', action: :destroy
post '/', action: :create
put '/', action: :update
end
get( resources :graphs, only: [:show] do
'/raw/*id', member do
to: 'raw#show', get :charts
constraints: { id: /.+/, format: /(html|js)/ }, get :commits
as: :raw get :ci
) get :languages
end
get( end
'/tree/*id',
to: 'tree#show', resources :branches, only: [:index, :new, :create, :destroy]
constraints: { id: /.+/, format: /(html|js)/ }, delete :merged_branches, controller: 'branches', action: :destroy_all_merged
as: :tree resources :tags, only: [:index, :show, :new, :create, :destroy] do
) resource :release, only: [:edit, :update]
end
get(
'/find_file/*id', resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::Regex.git_reference_regex } do
to: 'find_file#show', ## EE-specific
constraints: { id: /.+/, format: /html/ }, scope module: :protected_branches do
as: :find_file resources :merge_access_levels, only: [:destroy]
) resources :push_access_levels, only: [:destroy]
end
get( end
'/files/*id',
to: 'find_file#list', resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, end
as: :files
) scope constraints: { id: /.+/ } do
scope controller: :blob do
post( get '/new/*id', action: :new, as: :new_blob
'/create_dir/*id', post '/create/*id', action: :create, as: :create_blob
to: 'tree#create_dir', get '/edit/*id', action: :edit, as: :edit_blob
constraints: { id: /.+/ }, put '/update/*id', action: :update, as: :update_blob
as: 'create_dir' post '/preview/*id', action: :preview, as: :preview_blob
)
scope path: '/blob/*id', as: :blob do
get( get :diff
'/blame/*id', get '/', action: :show
to: 'blame#show', delete '/', action: :destroy
constraints: { id: /.+/, format: /(html|js)/ }, post '/', action: :create
as: :blame put '/', action: :update
) end
end
# File/dir history
get( get '/tree/*id', to: 'tree#show', as: :tree
'/commits/*id', get '/raw/*id', to: 'raw#show', as: :raw
to: 'commits#show', get '/blame/*id', to: 'blame#show', as: :blame
constraints: { id: /.+/, format: false }, get '/commits/*id', to: 'commits#show', as: :commits
as: :commits
) post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir
scope controller: :find_file do
get '/find_file/*id', action: :show, as: :find_file
get '/files/*id', action: :list, as: :files
end
end
end
...@@ -130,6 +130,7 @@ var config = { ...@@ -130,6 +130,7 @@ var config = {
'pdf_viewer', 'pdf_viewer',
'pipelines', 'pipelines',
'mr_widget_ee', 'mr_widget_ee',
'issue_show'
], ],
minChunks: function(module, count) { minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource); return module.resource && (/vue_shared/).test(module.resource);
......
...@@ -6,10 +6,6 @@ All technical content published by GitLab lives in the documentation, including: ...@@ -6,10 +6,6 @@ All technical content published by GitLab lives in the documentation, including:
- [User docs](#user-documentation): general documentation dedicated to regular users of GitLab - [User docs](#user-documentation): general documentation dedicated to regular users of GitLab
- [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances - [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances
- [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab - [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab
- [Topics](topics/index.md): pages organized per topic, gathering all the
resources already published by GitLab related to a specific subject, including
general docs, [technical articles](development/writing_documentation.md#technical-articles),
blog posts and video tutorials.
- [GitLab University](university/README.md): guides to learn Git and GitLab - [GitLab University](university/README.md): guides to learn Git and GitLab
through courses and videos. through courses and videos.
......
...@@ -40,6 +40,12 @@ The same process is valid for merge requests. Navigate to your project's **Merge ...@@ -40,6 +40,12 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee, and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label. milestone, and label.
## Search History
You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser.
![search history](img/search_history.gif)
### Shortcut ### Shortcut
You'll also find a shortcut on the search field on the top-right of the project's dashboard to You'll also find a shortcut on the search field on the top-right of the project's dashboard to
......
...@@ -10,6 +10,11 @@ in your GitLab instance sitewide. This configuration is optional, users will ...@@ -10,6 +10,11 @@ in your GitLab instance sitewide. This configuration is optional, users will
still be able to import their GitHub repositories with a still be able to import their GitHub repositories with a
[personal access token][gh-token]. [personal access token][gh-token].
>**Note:**
Administrators of a GitLab instance (Community or Enterprise Edition) can also
use the [GitHub rake task][gh-rake] to import projects from GitHub without the
constrains of a Sidekiq worker.
- At its current state, GitHub importer can import: - At its current state, GitHub importer can import:
- the repository description (GitLab 7.7+) - the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+) - the Git repository data (GitLab 7.7+)
...@@ -112,5 +117,6 @@ You can also choose a different name for the project and a different namespace, ...@@ -112,5 +117,6 @@ You can also choose a different name for the project and a different namespace,
if you have the privileges to do so. if you have the privileges to do so.
[gh-import]: ../../integration/github.md "GitHub integration" [gh-import]: ../../integration/github.md "GitHub integration"
[gh-rake]: ../../administration/raketasks/github_import.md "GitHub rake task"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration [gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token [gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
...@@ -75,3 +75,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' ...@@ -75,3 +75,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>r</kbd> | Reply (quoting selected text) | | <kbd>r</kbd> | Reply (quoting selected text) |
| <kbd>e</kbd> | Edit issue/merge request | | <kbd>e</kbd> | Edit issue/merge request |
| <kbd>l</kbd> | Change label | | <kbd>l</kbd> | Change label |
## Wiki pages
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>e</kbd> | Edit wiki page|
...@@ -87,7 +87,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -87,7 +87,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end end
step 'I search "hand"' do step 'I search "hand"' do
fill_in 'emoji_search', with: 'hand' fill_in 'emoji-menu-search', with: 'hand'
end end
step 'I see search result for "hand"' do step 'I see search result for "hand"' do
...@@ -101,7 +101,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -101,7 +101,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end end
step 'The search field is focused' do step 'The search field is focused' do
expect(page).to have_selector('#emoji_search') expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search') expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
end end
end end
...@@ -89,12 +89,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps ...@@ -89,12 +89,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end end
end end
step 'I should see project "Shop" version' do
page.within '.project-side' do
expect(page).to have_content '6.7.0.pre'
end
end
step 'change project default branch' do step 'change project default branch' do
select 'fix', from: 'project_default_branch' select 'fix', from: 'project_default_branch'
click_button 'Save changes' click_button 'Save changes'
......
...@@ -136,7 +136,8 @@ module Banzai ...@@ -136,7 +136,8 @@ module Banzai
nodes.each_with_object({}) do |node, hash| nodes.each_with_object({}) do |node, hash|
if node.has_attribute?(attribute) if node.has_attribute?(attribute)
hash[node] = objects_by_id[node.attr(attribute).to_i] obj = objects_by_id[node.attr(attribute).to_i]
hash[node] = obj if obj
end end
end end
end end
......
...@@ -172,7 +172,7 @@ module Ci ...@@ -172,7 +172,7 @@ module Ci
close_open_tags() close_open_tags()
OpenStruct.new( OpenStruct.new(
html: @out, html: @out.force_encoding(Encoding.default_external),
state: state, state: state,
append: append, append: append,
truncated: truncated, truncated: truncated,
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
# This was inspired from: http://stackoverflow.com/a/10219411/1520132 # This was inspired from: http://stackoverflow.com/a/10219411/1520132
class Stream class Stream
BUFFER_SIZE = 4096 BUFFER_SIZE = 4096
LIMIT_SIZE = 50.kilobytes LIMIT_SIZE = 500.kilobytes
attr_reader :stream attr_reader :stream
...@@ -14,6 +14,7 @@ module Gitlab ...@@ -14,6 +14,7 @@ module Gitlab
def initialize def initialize
@stream = yield @stream = yield
@stream&.binmode
end end
def valid? def valid?
...@@ -51,7 +52,7 @@ module Gitlab ...@@ -51,7 +52,7 @@ module Gitlab
read_last_lines(last_lines) read_last_lines(last_lines)
else else
stream.read stream.read
end end.force_encoding(Encoding.default_external)
end end
def html_with_state(state = nil) def html_with_state(state = nil)
...@@ -60,8 +61,8 @@ module Gitlab ...@@ -60,8 +61,8 @@ module Gitlab
def html(last_lines: nil) def html(last_lines: nil)
text = raw(last_lines: last_lines) text = raw(last_lines: last_lines)
stream = StringIO.new(text) buffer = StringIO.new(text)
::Ci::Ansi2html.convert(stream).html ::Ci::Ansi2html.convert(buffer).html
end end
def extract_coverage(regex) def extract_coverage(regex)
...@@ -113,7 +114,6 @@ module Gitlab ...@@ -113,7 +114,6 @@ module Gitlab
end end
chunks.join.lines.last(last_lines).join chunks.join.lines.last(last_lines).join
.force_encoding(Encoding.default_external)
end end
end end
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
# the user. We load as much as we can for encoding detection # the user. We load as much as we can for encoding detection
# (Linguist) and LFS pointer parsing. All other cases where we need full # (Linguist) and LFS pointer parsing. All other cases where we need full
# blob data should use load_all_data!. # blob data should use load_all_data!.
MAX_DATA_DISPLAY_SIZE = 10485760 MAX_DATA_DISPLAY_SIZE = 10.megabytes
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
...@@ -153,7 +153,7 @@ module Gitlab ...@@ -153,7 +153,7 @@ module Gitlab
def lfs_size def lfs_size
if has_lfs_version_key? if has_lfs_version_key?
size = data.match(/(?<=size )([0-9]+)/) size = data.match(/(?<=size )([0-9]+)/)
return size[1] if size return size[1].to_i if size
end end
nil nil
......
...@@ -52,7 +52,6 @@ class NewImporter < ::Gitlab::GithubImport::Importer ...@@ -52,7 +52,6 @@ class NewImporter < ::Gitlab::GithubImport::Importer
project.repository.add_remote(project.import_type, project.import_url) project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type) project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true) project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
rescue => e rescue => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
......
...@@ -633,10 +633,10 @@ describe 'Issues', feature: true do ...@@ -633,10 +633,10 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to have_content 'banana_sample' expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end end
it 'adds double newline to end of attachment markdown' do it "doesn't add double newline to end of a single attachment markdown" do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).to match /\n\n$/ expect(page.find_field("issue_description").value).not_to match /\n\n$/
end end
end end
......
require 'spec_helper'
feature 'Diff notes', js: true, feature: true do
include WaitForAjax
before do
login_as :admin
@merge_request = create(:merge_request)
@project = @merge_request.source_project
end
context 'merge request diffs' do
let(:comment_button_class) { '.add-diff-note' }
let(:notes_holder_input_class) { 'js-temp-notes-holder' }
let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
let(:test_note_comment) { 'this is a test note!' }
context 'when hovering over a parallel view diff file' do
before(:each) do
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
end
context 'with an old line on the left and no line on the right' do
it 'should allow commenting on the left side' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
end
it 'should not allow commenting on the right side' do
should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
end
end
context 'with no line on the left and a new line on the right' do
it 'should not allow commenting on the left side' do
should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
end
it 'should allow commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
end
end
context 'with an old line on the left and a new line on the right' do
it 'should allow commenting on the left side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
end
it 'should allow commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
end
end
context 'with an unchanged line on the left and an unchanged line on the right' do
it 'should allow commenting on the left side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
end
it 'should allow commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
end
end
context 'with a match line' do
it 'should not allow commenting on the left side' do
should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
end
it 'should not allow commenting on the right side' do
should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
end
end
context 'with an unfolded line' do
before(:each) do
find('.js-unfold', match: :first).click
wait_for_ajax
end
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="1"]') }
it 'should not allow commenting on the left side' do
should_not_allow_commenting(line_holder, 'left')
end
it 'should not allow commenting on the right side' do
should_not_allow_commenting(line_holder, 'right')
end
end
end
context 'when hovering over an inline view diff file' do
before do
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
end
context 'with a new line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'should not allow commenting' do
should_not_allow_commenting(find('.match', match: :first))
end
end
context 'with an unfolded line' do
before(:each) do
find('.js-unfold', match: :first).click
wait_for_ajax
end
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="1"]') }
it 'should not allow commenting' do
should_not_allow_commenting line_holder
end
end
context 'when hovering over a diff discussion' do
before do
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
end
it 'should not allow commenting' do
should_not_allow_commenting(find('.line_holder', match: :first))
end
end
end
context 'when the MR only supports legacy diff notes' do
before do
@merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
end
context 'with a new line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'should allow commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'should not allow commenting' do
should_not_allow_commenting(find('.match', match: :first))
end
end
end
def should_allow_commenting(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
expect(line[:num]).to have_css comment_button_class
comment_on_line(line_holder, line)
assert_comment_persistence(line_holder)
end
def should_not_allow_commenting(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
expect(line[:num]).not_to have_css comment_button_class
end
def get_line_components(line_holder, diff_side = nil)
if diff_side.nil?
get_inline_line_components(line_holder)
else
get_parallel_line_components(line_holder, diff_side)
end
end
def get_inline_line_components(line_holder)
{ content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
end
def get_parallel_line_components(line_holder, diff_side = nil)
side_index = diff_side == 'left' ? 0 : 1
# Wait for `.line_content`
line_holder.find('.line_content', match: :first)
# Wait for `.diff-line-num`
line_holder.find('.diff-line-num', match: :first)
{ content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
end
def comment_on_line(line_holder, line)
line[:num].find(comment_button_class).trigger 'click'
line_holder.find(:xpath, notes_holder_input_xpath)
notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
expect(notes_holder_input[:class]).to include(notes_holder_input_class)
notes_holder_input.fill_in 'note[note]', with: test_note_comment
click_button 'Comment'
wait_for_ajax
end
def assert_comment_persistence(line_holder)
expect(line_holder).to have_xpath notes_holder_input_xpath
notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
expect(notes_holder_saved).to have_content test_note_comment
end
end
end
require 'spec_helper'
feature 'Merge requests > User posts diff notes', :js do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
before do
project.add_developer(user)
login_as(user)
end
let(:comment_button_class) { '.add-diff-note' }
let(:notes_holder_input_class) { 'js-temp-notes-holder' }
let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
let(:test_note_comment) { 'this is a test note!' }
context 'when hovering over a parallel view diff file' do
before do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'parallel')
end
context 'with an old line on the left and no line on the right' do
it 'allows commenting on the left side' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
end
it 'does not allow commenting on the right side' do
should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
end
end
context 'with no line on the left and a new line on the right' do
it 'does not allow commenting on the left side' do
should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
end
end
context 'with an old line on the left and a new line on the right' do
it 'allows commenting on the left side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
end
end
context 'with an unchanged line on the left and an unchanged line on the right' do
it 'allows commenting on the left side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
end
it 'allows commenting on the right side' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
end
end
context 'with a match line' do
it 'does not allow commenting on the left side' do
should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
end
it 'does not allow commenting on the right side' do
should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
end
end
context 'with an unfolded line' do
before(:each) do
find('.js-unfold', match: :first).click
wait_for_ajax
end
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="1"]') }
it 'does not allow commenting on the left side' do
should_not_allow_commenting(line_holder, 'left')
end
it 'does not allow commenting on the right side' do
should_not_allow_commenting(line_holder, 'right')
end
end
end
context 'when hovering over an inline view diff file' do
before do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
end
context 'with a new line' do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'allows commenting' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'does not allow commenting' do
should_not_allow_commenting(find('.match', match: :first))
end
end
context 'with an unfolded line' do
before(:each) do
find('.js-unfold', match: :first).click
wait_for_ajax
end
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="1"]') }
it 'does not allow commenting' do
should_not_allow_commenting line_holder
end
end
context 'when hovering over a diff discussion' do
before do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'does not allow commenting' do
should_not_allow_commenting(find('.line_holder', match: :first))
end
end
end
context 'when cancelling the comment addition' do
before do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
end
context 'with a new line' do
it 'allows dismissing a comment' do
should_allow_dismissing_a_comment(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
end
describe 'with muliple note forms' do
before do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
click_diff_line(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
describe 'posting a note' do
it 'adds as discussion' do
expect(page).to have_css('.js-temp-notes-holder', count: 2)
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
expect(page).to have_css('.notes_holder .note', count: 1)
expect(page).to have_css('.js-temp-notes-holder', count: 1)
expect(page).to have_button('Reply...')
end
end
end
context 'when the MR only supports legacy diff notes' do
before do
merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: 'inline')
end
context 'with a new line' do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
end
end
context 'with an old line' do
it 'allows commenting' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
end
end
context 'with an unchanged line' do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
end
end
context 'with a match line' do
it 'does not allow commenting' do
should_not_allow_commenting(find('.match', match: :first))
end
end
end
def should_allow_commenting(line_holder, diff_side = nil, asset_form_reset: true)
write_comment_on_line(line_holder, diff_side)
click_button 'Comment'
wait_for_ajax
assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset)
end
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
find('.js-close-discussion-note-form').trigger('click')
assert_comment_dismissal(line_holder)
end
def should_not_allow_commenting(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
expect(line[:num]).not_to have_css comment_button_class
end
def get_line_components(line_holder, diff_side = nil)
if diff_side.nil?
get_inline_line_components(line_holder)
else
get_parallel_line_components(line_holder, diff_side)
end
end
def get_inline_line_components(line_holder)
{ content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
end
def get_parallel_line_components(line_holder, diff_side = nil)
side_index = diff_side == 'left' ? 0 : 1
# Wait for `.line_content`
line_holder.find('.line_content', match: :first)
# Wait for `.diff-line-num`
line_holder.find('.diff-line-num', match: :first)
{ content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
end
def click_diff_line(line_holder, diff_side = nil)
line = get_line_components(line_holder, diff_side)
line[:content].hover
expect(line[:num]).to have_css comment_button_class
line[:num].find(comment_button_class).trigger 'click'
end
def write_comment_on_line(line_holder, diff_side)
click_diff_line(line_holder, diff_side)
notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
expect(notes_holder_input[:class]).to include(notes_holder_input_class)
notes_holder_input.fill_in 'note[note]', with: test_note_comment
end
def assert_comment_persistence(line_holder, asset_form_reset:)
notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
expect(notes_holder_saved).to have_content test_note_comment
assert_form_is_reset if asset_form_reset
end
def assert_comment_dismissal(line_holder)
expect(line_holder).not_to have_xpath notes_holder_input_xpath
expect(page).not_to have_content test_note_comment
assert_form_is_reset
end
def assert_form_is_reset
expect(page).to have_no_css('.js-temp-notes-holder')
end
end
require 'spec_helper'
describe 'Merge requests > User posts notes', :js do
let(:project) { create(:project) }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
let!(:note) do
create(:note_on_merge_request, :with_attachment, noteable: merge_request,
project: project)
end
before do
login_as :admin
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
subject { page }
describe 'the note form' do
it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form .js-comment-button').value).
to eq('Comment')
page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel')
end
end
describe 'with text' do
before do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awesome'
end
end
it 'has enable submit button and preview button' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
end
end
end
end
describe 'when posting a note' do
before do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awesome!'
find('.js-md-preview-button').click
click_button 'Comment'
end
end
it 'is added and form reset' do
is_expected.to have_content('This is awesome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
expect(page).to have_css('.js-md-preview', visible: :hidden)
end
page.within('.js-main-target-form') do
is_expected.to have_css('.js-note-text', visible: true)
end
end
end
describe 'when editing a note' do
it 'there should be a hidden edit form' do
is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
end
describe 'editing the note' do
before do
find('.note').hover
find('.js-note-edit').click
end
it 'shows the note edit form and hide the note body' do
page.within("#note_#{note.id}") do
expect(find('.current-note-edit-form', visible: true)).to be_visible
expect(find('.note-edit-form', visible: true)).to be_visible
expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible
end
end
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
expect(find('.js-note-text', visible: false).text).to eq ''
end
end
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'This is the new content'
find('.btn-save').click
end
find('.note').hover
find('.js-note-edit').click
page.within('.current-note-edit-form') do
expect(find('#note_note').value).to eq('This is the new content')
find('.js-md:first-child').click
expect(find('#note_note').value).to eq('This is the new content****')
end
end
it 'appends the edited at time to the note' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-save').click
end
page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text).
to match(/less than a minute ago/)
end
end
end
describe 'deleting an attachment' do
before do
find('.note').hover
find('.js-note-edit').click
end
it 'shows the delete link' do
page.within('.note-attachment') do
is_expected.to have_css('.js-note-attachment-delete')
end
end
it 'removes the attachment div and resets the edit form' do
find('.js-note-attachment-delete').click
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
wait_for_ajax
end
end
end
end
require 'spec_helper'
feature 'Merge requests > User sees system notes' do
let(:public_project) { create(:project, :public) }
let(:private_project) { create(:project, :private) }
let(:issue) { create(:issue, project: private_project) }
let(:merge_request) { create(:merge_request, source_project: public_project, source_branch: 'markdown') }
let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: public_project, note: "mentioned in #{issue.to_reference(public_project)}") }
context 'when logged-in as a member of the private project' do
before do
user = create(:user)
private_project.add_developer(user)
login_as(user)
end
it 'shows the system note' do
visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request)
expect(page).to have_css('.system-note')
end
end
context 'when not logged-in' do
it 'hides the system note' do
visit namespace_project_merge_request_path(public_project.namespace, public_project, merge_request)
expect(page).not_to have_css('.system-note')
end
end
end
require 'spec_helper'
describe 'Comments', feature: true do
include RepoHelpers
include WaitForAjax
describe 'On a merge request', js: true, feature: true do
let!(:project) { create(:project) }
let!(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
let!(:note) do
create(:note_on_merge_request, :with_attachment, noteable: merge_request,
project: project)
end
before do
login_as :admin
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
subject { page }
describe 'the note form' do
it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form .js-comment-button').value).
to eq('Comment')
page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel')
end
end
describe 'with text' do
before do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awesome'
end
end
it 'has enable submit button and preview button' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
end
end
end
end
describe 'when posting a note' do
before do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: 'This is awsome!'
find('.js-md-preview-button').click
click_button 'Comment'
end
end
it 'is added and form reset' do
is_expected.to have_content('This is awsome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
expect(page).to have_css('.js-md-preview', visible: :hidden)
end
page.within('.js-main-target-form') do
is_expected.to have_css('.js-note-text', visible: true)
end
end
end
describe 'when editing a note', js: true do
it 'there should be a hidden edit form' do
is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
is_expected.to have_css('.note-edit-form.mr-note-edit-form', visible: false, count: 1)
end
describe 'editing the note' do
before do
find('.note').hover
find('.js-note-edit').click
end
it 'shows the note edit form and hide the note body' do
page.within("#note_#{note.id}") do
expect(find('.current-note-edit-form', visible: true)).to be_visible
expect(find('.note-edit-form', visible: true)).to be_visible
expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible
end
end
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
expect(find('.js-note-text', visible: false).text).to eq ''
end
end
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'This is the new content'
find('.btn-save').click
end
find('.note').hover
find('.js-note-edit').click
page.within('.current-note-edit-form') do
expect(find('#note_note').value).to eq('This is the new content')
find('.js-md:first-child').click
expect(find('#note_note').value).to eq('This is the new content****')
end
end
it 'appends the edited at time to the note' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-save').click
end
page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text).
to match(/less than a minute ago/)
end
end
end
describe 'deleting an attachment' do
before do
find('.note').hover
find('.js-note-edit').click
end
it 'shows the delete link' do
page.within('.note-attachment') do
is_expected.to have_css('.js-note-attachment-delete')
end
end
it 'removes the attachment div and resets the edit form' do
find('.js-note-attachment-delete').click
is_expected.not_to have_css('.note-attachment')
is_expected.not_to have_css('.current-note-edit-form')
wait_for_ajax
end
end
end
end
describe 'Handles cross-project system notes', js: true, feature: true do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:project2) { create(:project, :private) }
let(:issue) { create(:issue, project: project2) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
it 'shows the system note' do
login_as :admin
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(page).to have_css('.system-note')
end
it 'hides redacted system note' do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(page).not_to have_css('.system-note')
end
end
describe 'On a merge request diff', js: true, feature: true do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
before do
login_as :admin
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
subject { page }
describe 'when adding a note' do
before do
click_diff_line
end
describe 'the notes holder' do
it { is_expected.to have_css('.js-temp-notes-holder') }
it 'has .new_note css class' do
page.within('.js-temp-notes-holder') do
expect(subject).to have_css('.new-note')
end
end
end
describe 'the note form' do
it "does not add a second form for same row" do
click_diff_line
is_expected.
to have_css("form[data-line-code='#{line_code}']",
count: 1)
end
it 'is removed when canceled' do
is_expected.to have_css('.js-temp-notes-holder')
page.within("form[data-line-code='#{line_code}']") do
find('.js-close-discussion-note-form').trigger('click')
end
is_expected.to have_no_css('.js-temp-notes-holder')
end
end
end
describe 'with muliple note forms' do
before do
click_diff_line
click_diff_line(line_code_2)
end
it { is_expected.to have_css('.js-temp-notes-holder', count: 2) }
describe 'previewing them separately' do
before do
# add two separate texts and trigger previews on both
page.within("tr[id='#{line_code}'] + .js-temp-notes-holder") do
fill_in 'note[note]', with: 'One comment on line 7'
find('.js-md-preview-button').click
end
page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
fill_in 'note[note]', with: 'Another comment on line 10'
find('.js-md-preview-button').click
end
end
end
describe 'posting a note' do
before do
page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
fill_in 'note[note]', with: 'Another comment on line 10'
click_button('Comment')
end
end
it 'adds as discussion' do
is_expected.to have_content('Another comment on line 10')
is_expected.to have_css('.notes_holder')
is_expected.to have_css('.notes_holder .note', count: 1)
is_expected.to have_button('Reply...')
end
it 'adds code to discussion' do
click_button 'Reply...'
page.within(first('.js-discussion-note-form')) do
fill_in 'note[note]', with: '```{{ test }}```'
click_button('Comment')
end
expect(page).to have_content('{{ test }}')
end
end
end
end
def line_code
sample_compare.changes.first[:line_code]
end
def line_code_2
sample_compare.changes.last[:line_code]
end
def click_diff_line(data = line_code)
find(".line_holder[id='#{data}'] td.line_content").hover
find(".line_holder[id='#{data}'] button").trigger('click')
end
end
require 'spec_helper'
feature 'Wiki shortcuts', :feature, :js do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:wiki_page) do
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
end
before do
login_as(user)
visit namespace_project_wiki_path(project.namespace, project, wiki_page)
end
scenario 'Visit edit wiki page using "e" keyboard shortcut' do
find('body').native.send_key('e')
expect(find('.wiki-page-title')).to have_content('Edit Page')
end
end
...@@ -274,4 +274,27 @@ describe ProjectsHelper do ...@@ -274,4 +274,27 @@ describe ProjectsHelper do
end end
end end
end end
describe "#visibility_select_options" do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it "does not include the Public restricted level" do
expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).not_to include('Public')
end
it "includes the Internal level" do
expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Internal')
end
it "includes the Private level" do
expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private')
end
end
end end
...@@ -65,7 +65,7 @@ require('~/lib/utils/common_utils'); ...@@ -65,7 +65,7 @@ require('~/lib/utils/common_utils');
$emojiMenu = $('.emoji-menu'); $emojiMenu = $('.emoji-menu');
expect($emojiMenu.length).toBe(1); expect($emojiMenu.length).toBe(1);
expect($emojiMenu.hasClass('is-visible')).toBe(true); expect($emojiMenu.hasClass('is-visible')).toBe(true);
expect($emojiMenu.find('#emoji_search').length).toBe(1); expect($emojiMenu.find('.js-emoji-menu-search').length).toBe(1);
return expect($('.js-awards-block.current').length).toBe(1); return expect($('.js-awards-block.current').length).toBe(1);
}); });
}); });
...@@ -217,16 +217,35 @@ require('~/lib/utils/common_utils'); ...@@ -217,16 +217,35 @@ require('~/lib/utils/common_utils');
return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
}); });
}); });
describe('search', function() { describe('::searchEmojis', () => {
return it('should filter the emoji', function(done) { it('should filter the emoji', function(done) {
return openAndWaitForEmojiMenu() return openAndWaitForEmojiMenu()
.then(() => { .then(() => {
expect($('[data-name=angel]').is(':visible')).toBe(true); expect($('[data-name=angel]').is(':visible')).toBe(true);
expect($('[data-name=anger]').is(':visible')).toBe(true); expect($('[data-name=anger]').is(':visible')).toBe(true);
$('#emoji_search').val('ali').trigger('input'); awardsHandler.searchEmojis('ali');
expect($('[data-name=angel]').is(':visible')).toBe(false); expect($('[data-name=angel]').is(':visible')).toBe(false);
expect($('[data-name=anger]').is(':visible')).toBe(false); expect($('[data-name=anger]').is(':visible')).toBe(false);
expect($('[data-name=alien]').is(':visible')).toBe(true); expect($('[data-name=alien]').is(':visible')).toBe(true);
expect($('.js-emoji-menu-search').val()).toBe('ali');
})
.then(done)
.catch((err) => {
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
it('should clear the search when searching for nothing', function(done) {
return openAndWaitForEmojiMenu()
.then(() => {
awardsHandler.searchEmojis('ali');
expect($('[data-name=angel]').is(':visible')).toBe(false);
expect($('[data-name=anger]').is(':visible')).toBe(false);
expect($('[data-name=alien]').is(':visible')).toBe(true);
awardsHandler.searchEmojis('');
expect($('[data-name=angel]').is(':visible')).toBe(true);
expect($('[data-name=anger]').is(':visible')).toBe(true);
expect($('[data-name=alien]').is(':visible')).toBe(true);
expect($('.js-emoji-menu-search').val()).toBe('');
}) })
.then(done) .then(done)
.catch((err) => { .catch((err) => {
...@@ -234,6 +253,7 @@ require('~/lib/utils/common_utils'); ...@@ -234,6 +253,7 @@ require('~/lib/utils/common_utils');
}); });
}); });
}); });
describe('emoji menu', function() { describe('emoji menu', function() {
const emojiSelector = '[data-name="sunglasses"]'; const emojiSelector = '[data-name="sunglasses"]';
const openEmojiMenuAndAddEmoji = function() { const openEmojiMenuAndAddEmoji = function() {
......
...@@ -107,4 +107,44 @@ describe('List model', () => { ...@@ -107,4 +107,44 @@ describe('List model', () => {
expect(gl.boardService.moveIssue) expect(gl.boardService.moveIssue)
.toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined); .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
}); });
describe('page number', () => {
beforeEach(() => {
spyOn(list, 'getIssues');
});
it('increase page number if current issue count is more than the page size', () => {
for (let i = 0; i < 30; i += 1) {
list.issues.push(new ListIssue({
title: 'Testing',
iid: _.random(10000) + i,
confidential: false,
labels: [list.label]
}));
}
list.issuesSize = 50;
expect(list.issues.length).toBe(30);
list.nextPage();
expect(list.page).toBe(2);
expect(list.getIssues).toHaveBeenCalled();
});
it('does not increase page number if issue count is less than the page size', () => {
list.issues.push(new ListIssue({
title: 'Testing',
iid: _.random(10000),
confidential: false,
labels: [list.label]
}));
list.issuesSize = 2;
list.nextPage();
expect(list.page).toBe(1);
expect(list.getIssues).toHaveBeenCalled();
});
});
}); });
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global Build */ /* global Build */
import { bytesToKiB } from '~/lib/utils/number_utils';
require('~/lib/utils/datetime_utility'); import '~/lib/utils/datetime_utility';
require('~/lib/utils/url_utility'); import '~/lib/utils/url_utility';
require('~/build'); import '~/build';
require('~/breakpoints'); import '~/breakpoints';
require('vendor/jquery.nicescroll'); import 'vendor/jquery.nicescroll';
describe('Build', () => { describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
...@@ -144,24 +144,6 @@ describe('Build', () => { ...@@ -144,24 +144,6 @@ describe('Build', () => {
expect($('#build-trace .js-build-output').text()).toMatch(/Different/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
}); });
it('shows information about truncated log', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
truncated: true,
size: '50',
});
expect(
$('#build-trace .js-truncated-info').text().trim(),
).toContain('Showing last 50 KiB of log');
expect($('#build-trace .js-truncated-info-size').text()).toMatch('50');
});
it('reloads the page when the build is done', () => { it('reloads the page when the build is done', () => {
spyOn(gl.utils, 'visitUrl'); spyOn(gl.utils, 'visitUrl');
...@@ -176,6 +158,107 @@ describe('Build', () => { ...@@ -176,6 +158,107 @@ describe('Build', () => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
}); });
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
});
it('shows the size in KiB', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
const size = 50;
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(size)}`);
});
it('shows incremented size', () => {
jasmine.clock().tick(4001);
let args = $.ajax.calls.argsFor(0)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(50)}`);
jasmine.clock().tick(4001);
args = $.ajax.calls.argsFor(2)[0];
args.success.call($, {
html: '<span>Update</span>',
status: 'success',
append: true,
size: 10,
total: 100,
});
expect(
document.querySelector('.js-truncated-info-size').textContent.trim(),
).toEqual(`${bytesToKiB(60)}`);
});
it('renders the raw link', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
});
expect(
document.querySelector('.js-raw-link').textContent.trim(),
).toContain('Complete Raw');
});
});
describe('when size is equal than total', () => {
it('does not show the trunctated information', () => {
jasmine.clock().tick(4001);
const [{ success }] = $.ajax.calls.argsFor(0);
success.call($, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
});
expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
});
});
});
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import issueTitle from '~/issue_show/issue_title'; import issueTitle from '~/issue_show/issue_title.vue';
describe('Issue Title', () => { describe('Issue Title', () => {
let IssueTitleComponent; let IssueTitleComponent;
......
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => { describe('Number Utils', () => {
describe('formatRelevantDigits', () => { describe('formatRelevantDigits', () => {
...@@ -38,4 +38,11 @@ describe('Number Utils', () => { ...@@ -38,4 +38,11 @@ describe('Number Utils', () => {
expect(leftFromDecimal.length).toBe(3); expect(leftFromDecimal.length).toBe(3);
}); });
}); });
describe('bytesToKiB', () => {
it('calculates KiB for the given bytes', () => {
expect(bytesToKiB(1024)).toEqual(1);
expect(bytesToKiB(1000)).toEqual(0.9765625);
});
});
}); });
...@@ -114,8 +114,27 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do ...@@ -114,8 +114,27 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
expect(hash).to eq({ link => user }) expect(hash).to eq({ link => user })
end end
it 'returns an empty Hash when the list of nodes is empty' do it 'returns an empty Hash when entry does not exist in the database' do
expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({}) link = double(:link)
expect(link).to receive(:has_attribute?).
with('data-user').
and_return(true)
expect(link).to receive(:attr).
with('data-user').
and_return('1')
nodes = [link]
bad_id = user.id + 100
expect(subject).to receive(:unique_attribute_values).
with(nodes, 'data-user').
and_return([bad_id.to_s])
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
expect(hash).to eq({})
end end
end end
......
...@@ -43,24 +43,48 @@ describe Gitlab::Ci::Trace::Stream do ...@@ -43,24 +43,48 @@ describe Gitlab::Ci::Trace::Stream do
it 'forwards to the next linefeed, case 1' do it 'forwards to the next linefeed, case 1' do
stream.limit(7) stream.limit(7)
expect(stream.raw).to eq('') result = stream.raw
expect(result).to eq('')
expect(result.encoding).to eq(Encoding.default_external)
end end
it 'forwards to the next linefeed, case 2' do it 'forwards to the next linefeed, case 2' do
stream.limit(29) stream.limit(29)
expect(stream.raw).to eq("\e[01;32m許功蓋\e[0m\n") result = stream.raw
expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
expect(result.encoding).to eq(Encoding.default_external)
end
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
it 'reads in binary, output as Encoding.default_external' do
stream.limit(52)
result = stream.html
expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
expect(result.encoding).to eq(Encoding.default_external)
end end
end end
end end
describe '#append' do describe '#append' do
let(:tempfile) { Tempfile.new }
let(:stream) do let(:stream) do
described_class.new do described_class.new do
StringIO.new("12345678") tempfile.write("12345678")
tempfile.rewind
tempfile
end end
end end
after do
tempfile.unlink
end
it "truncates and append content" do it "truncates and append content" do
stream.append("89", 4) stream.append("89", 4)
stream.seek(0) stream.seek(0)
...@@ -68,6 +92,17 @@ describe Gitlab::Ci::Trace::Stream do ...@@ -68,6 +92,17 @@ describe Gitlab::Ci::Trace::Stream do
expect(stream.size).to eq(6) expect(stream.size).to eq(6)
expect(stream.raw).to eq("123489") expect(stream.raw).to eq("123489")
end end
it 'appends in binary mode' do
'😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
stream.append(byte, offset)
end
stream.seek(0)
expect(stream.size).to eq(4)
expect(stream.raw).to eq('😺')
end
end end
describe '#set' do describe '#set' do
......
...@@ -234,7 +234,7 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -234,7 +234,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it { expect(blob.lfs_pointer?).to eq(true) } it { expect(blob.lfs_pointer?).to eq(true) }
it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") } it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
it { expect(blob.lfs_size).to eq("19548") } it { expect(blob.lfs_size).to eq(19548) }
it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") } it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
it { expect(blob.name).to eq("image.jpg") } it { expect(blob.name).to eq("image.jpg") }
it { expect(blob.path).to eq("files/lfs/image.jpg") } it { expect(blob.path).to eq("files/lfs/image.jpg") }
...@@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do ...@@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it { expect(blob.lfs_pointer?).to eq(false) } it { expect(blob.lfs_pointer?).to eq(false) }
it { expect(blob.lfs_oid).to eq(nil) } it { expect(blob.lfs_oid).to eq(nil) }
it { expect(blob.lfs_size).to eq("1575078") } it { expect(blob.lfs_size).to eq(1575078) }
it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") } it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
it { expect(blob.name).to eq("picture-invalid.png") } it { expect(blob.name).to eq("picture-invalid.png") }
it { expect(blob.path).to eq("files/lfs/picture-invalid.png") } it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
......
...@@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do ...@@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do
it 'lets a worker delete the user' do it 'lets a worker delete the user' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
delete_solo_owned_groups: true) delete_solo_owned_groups: true,
hard_delete: true)
subject.remove_user(deleted_by: user) subject.remove_user(deleted_by: user)
end end
......
...@@ -171,6 +171,27 @@ describe Repository, models: true do ...@@ -171,6 +171,27 @@ describe Repository, models: true do
end end
end end
describe '#commits' do
it 'sets follow when path is a single path' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
repository.commits('master', path: 'README.md')
repository.commits('master', path: ['README.md'])
end
it 'does not set follow when path is multiple paths' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
repository.commits('master', path: ['README.md', 'CHANGELOG'])
end
it 'does not set follow when there are no paths' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
repository.commits('master')
end
end
describe '#find_commits_by_message' do describe '#find_commits_by_message' do
it 'returns commits with messages containing a given string' do it 'returns commits with messages containing a given string' do
commit_ids = repository.find_commits_by_message('submodule').map(&:id) commit_ids = repository.find_commits_by_message('submodule').map(&:id)
...@@ -1285,7 +1306,6 @@ describe Repository, models: true do ...@@ -1285,7 +1306,6 @@ describe Repository, models: true do
:changelog, :changelog,
:license, :license,
:contributing, :contributing,
:version,
:gitignore, :gitignore,
:koding, :koding,
:gitlab_ci, :gitlab_ci,
......
require 'spec_helper' require 'spec_helper'
describe SpamLog, models: true do describe SpamLog, models: true do
let(:admin) { create(:admin) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
end end
...@@ -13,13 +15,18 @@ describe SpamLog, models: true do ...@@ -13,13 +15,18 @@ describe SpamLog, models: true do
it 'blocks the user' do it 'blocks the user' do
spam_log = build(:spam_log) spam_log = build(:spam_log)
expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true) expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end end
it 'removes the user' do it 'removes the user' do
spam_log = build(:spam_log) spam_log = build(:spam_log)
user = spam_log.user
Sidekiq::Testing.inline! do
spam_log.remove_user(deleted_by: admin)
end
expect { spam_log.remove_user }.to change { User.count }.by(-1) expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
end end
...@@ -484,7 +484,7 @@ describe 'project routing' do ...@@ -484,7 +484,7 @@ describe 'project routing' do
end end
it 'to #list' do it 'to #list' do
expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.json')
end end
end end
......
...@@ -54,6 +54,15 @@ describe Projects::ImportService, services: true do ...@@ -54,6 +54,15 @@ describe Projects::ImportService, services: true do
expect(result[:status]).to eq :error expect(result[:status]).to eq :error
expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
end end
it 'does not remove the GitHub remote' do
expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
subject.execute
expect(project.repository.raw_repository.remote_names).to include('github')
end
end end
context 'with a non Github repository' do context 'with a non Github repository' do
......
...@@ -152,6 +152,12 @@ describe Users::DestroyService, services: true do ...@@ -152,6 +152,12 @@ describe Users::DestroyService, services: true do
service.execute(user) service.execute(user)
end end
it 'does not run `MigrateToGhostUser` if hard_delete option is given' do
expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute)
service.execute(user, hard_delete: true)
end
end end
end 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