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:
- 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_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
......@@ -92,6 +93,7 @@ stages:
- 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_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- 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)'
artifacts:
......
......@@ -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
## 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,
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.
### 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
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
information, see
[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)
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.
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:
1. a Release Manager
......
......@@ -8,6 +8,7 @@ import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
const requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
......@@ -105,8 +106,9 @@ function AwardsHandler() {
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$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) {
if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active');
$menu.removeClass('is-visible');
$('#emoji_search').blur();
$('.js-emoji-menu-search').blur();
} else {
$addBtn.addClass('is-active');
this.positionMenu($menu, $addBtn);
$menu.addClass('is-visible');
$('#emoji_search').focus();
$('.js-emoji-menu-search').focus();
}
} else {
$addBtn.addClass('is-loading is-active');
......@@ -147,7 +149,7 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
$createdMenu.addClass('is-visible');
$('#emoji_search').focus();
$('.js-emoji-menu-search').focus();
}, 200);
});
}
......@@ -180,7 +182,7 @@ AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) {
const emojiMenuMarkup = `
<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">
${frequentlyUsedCatgegory}
......@@ -500,24 +502,41 @@ AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmoj
};
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();
// 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.searchEmojis(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();
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
if (e.target === e.currentTarget) {
// Clear the search
this.searchEmojis('');
}
});
};
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 namesMatchingAlias = [];
......
......@@ -22,6 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.target.classList.toggle('open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
......
......@@ -3,6 +3,8 @@
/* global ListLabel */
import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List {
constructor (obj) {
this.id = obj.id;
......@@ -58,7 +60,9 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
this.page += 1;
if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
}
return this.getIssues(false);
}
......@@ -146,10 +150,7 @@ class List {
}
updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
.then(() => {
listFrom.getIssues(false);
});
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid);
}
findIssue (id) {
......
......@@ -2,6 +2,8 @@
consistent-return, prefer-rest-params */
/* global Breakpoints */
import { bytesToKiB } from './lib/utils/number_utils';
const bind = function (fn, me) { return function () { return fn.apply(me, arguments); }; };
const AUTO_SCROLL_OFFSET = 75;
const DOWN_BUILD_TRACE = '#down-build-trace';
......@@ -20,6 +22,7 @@ window.Build = (function () {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
this.logBytes = 0;
this.updateDropdown = bind(this.updateDropdown, this);
......@@ -98,15 +101,22 @@ window.Build = (function () {
if (log.append) {
$buildContainer.append(log.html);
this.logBytes += log.size;
} else {
$buildContainer.html(log.html);
if (log.truncated) {
$('.js-truncated-info-size').html(` ${log.size} `);
this.$truncatedInfo.removeClass('hidden');
this.initAffixTruncatedInfo();
} else {
this.$truncatedInfo.addClass('hidden');
}
this.logBytes = log.size;
}
// if the incremental sum of logBytes we received is less than the total
// we need to show a message warning the user about that.
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();
......
......@@ -125,7 +125,7 @@ $(() => {
},
dismissOverviewDialog() {
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({
return {
discussions: CommentsStore.state,
loading: false,
note: {},
};
},
watch: {
......@@ -33,6 +32,9 @@ const ResolveBtn = Vue.extend({
discussion: function () {
return this.discussions[this.discussionId];
},
note: function () {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
buttonText: function () {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
......@@ -111,8 +113,6 @@ const ResolveBtn = Vue.extend({
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
});
this.note = this.discussion.getNote(this.noteId);
}
});
......
......@@ -35,6 +35,7 @@
/* global Sidebar */
/* global WeightSelect */
/* global AdminEmailSelect */
/* global ShortcutsWiki */
import Issue from './issue';
......@@ -49,6 +50,7 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
......@@ -446,7 +448,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
break;
case 'wikis':
new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'));
break;
......
......@@ -66,7 +66,10 @@ window.DropzoneInput = (function() {
form_textarea.focus();
},
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) {
var checkIfMsgExists, errorAlert;
......@@ -123,12 +126,15 @@ window.DropzoneInput = (function() {
}
return false;
};
pasteText = function(text) {
pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text + "\n\n";
var formattedText = text;
if (shouldPad) formattedText += "\n\n";
const textarea = child.get(0);
caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
......
import Vue from 'vue';
import IssueTitle from './issue_title';
import IssueTitle from './issue_title.vue';
import '../vue_shared/vue_resource_interceptor';
const vueOptions = () => ({
el: '.issue-title-entrypoint',
components: {
IssueTitle,
},
data() {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
(() => {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
const { initialTitle, endpoint } = issueTitleData;
return {
initialTitle: issueTitleData.initialTitle,
endpoint: issueTitleData.endpoint,
};
},
template: `
<IssueTitle
:initialTitle="initialTitle"
:endpoint="endpoint"
/>
`,
});
const vm = new Vue({
el: '.issue-title-entrypoint',
render: createElement => createElement(IssueTitle, {
props: {
initialTitle,
endpoint,
},
}),
});
(() => new Vue(vueOptions()))();
return vm;
})();
<script>
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
......@@ -72,7 +73,9 @@ export default {
created() {
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
......@@ -32,3 +32,13 @@ export function formatRelevantDigits(number) {
}
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 {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
Cookies.set(USER_CALLOUT_COOKIE, 'true');
Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
......
......@@ -155,7 +155,7 @@ header {
.header-logo {
display: inline-block;
margin: 0 7px 0 2px;
margin: 0 12px 0 2px;
position: relative;
top: 10px;
transition-duration: .3s;
......@@ -186,7 +186,7 @@ header {
display: flex;
align-items: flex-start;
flex: 1 1 auto;
padding-top: (($header-height - 19) / 2);
padding-top: 14px;
overflow: hidden;
}
......
......@@ -61,8 +61,9 @@
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black-transparent;
background-color: $black;
height: 45px;
padding: 15px;
&.affix {
top: 0;
......@@ -87,6 +88,16 @@
right: 5px;
left: 5px;
}
.truncated-info-size {
margin: 0 5px;
}
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
}
......
......@@ -135,7 +135,7 @@
.text-expander {
display: inline-block;
background: $gray-light;
background: $white-light;
color: $gl-text-color-secondary;
padding: 0 5px;
cursor: pointer;
......@@ -146,6 +146,11 @@
line-height: $gl-font-size;
outline: none;
&.open {
background: $gray-light;
box-shadow: inset 0 0 2px rgba($black, 0.2);
}
&:hover {
background-color: darken($gray-light, 10%);
text-decoration: none;
......
......@@ -106,6 +106,10 @@
span {
white-space: pre-wrap;
}
.line {
word-wrap: break-word;
}
}
}
......
......@@ -123,6 +123,9 @@ ul.notes {
}
.note-emoji-button {
position: relative;
line-height: 1;
.fa-spinner {
display: none;
}
......@@ -352,6 +355,15 @@ ul.notes {
font-size: 14px;
}
.note-header {
display: flex;
justify-content: space-between;
}
.note-header-info {
min-width: 0;
}
.note-headline-light {
display: inline;
......@@ -371,21 +383,27 @@ ul.notes {
}
}
.note-headline-meta {
display: inline-block;
white-space: nowrap;
}
/**
* Actions for Discussions/Notes
*/
.discussion-actions,
.note-actions {
.discussion-actions {
float: right;
margin-left: 10px;
color: $gray-darkest;
}
.note-actions {
position: absolute;
right: 0;
top: 0;
flex-shrink: 0;
// For PhantomJS that does not support flex
float: right;
margin-left: 10px;
color: $gray-darkest;
.note-action-button {
margin-left: 8px;
......@@ -428,7 +446,8 @@ ul.notes {
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
margin-left: -20px;
top: 0;
left: 0;
opacity: 0;
}
......
......@@ -970,27 +970,23 @@ a.allowed-to-push {
}
.variable-key {
width: 300px;
max-width: 300px;
max-width: 120px;
overflow: hidden;
word-wrap: break-word;
// override bootstrap
white-space: normal!important;
@media (max-width: $screen-sm-max) {
width: 150px;
max-width: 150px;
}
white-space: nowrap;
text-overflow: ellipsis;
}
.variable-value {
@media(max-width: $screen-xs-max) {
width: 150px;
max-width: 150px;
overflow: hidden;
word-wrap: break-word;
}
max-width: 150px;
overflow: hidden;
word-wrap: break-word;
white-space: nowrap;
text-overflow: ellipsis;
}
.variable-menu {
text-align: right;
}
}
......
......@@ -7,7 +7,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
spam_log = SpamLog.find(params[:id])
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."
else
spam_log.destroy
......
......@@ -34,7 +34,7 @@ module BlobHelper
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' }
# 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"
elsif current_user && can?(current_user, :fork_project, project)
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
......@@ -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' }
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' }
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'
elsif can?(current_user, :fork_project, project)
continue_params = {
......@@ -90,7 +90,7 @@ module BlobHelper
)
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)
end
......
......@@ -24,7 +24,7 @@ module ProjectsHelper
return "(deleted)" unless author
author_html = ""
author_html = ""
# 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]
......@@ -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
else
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
......@@ -457,13 +457,22 @@ module ProjectsHelper
end
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),
{ data: { description: visibility_level_description(level, project) } },
level
]
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
......@@ -42,7 +42,7 @@ module SnippetsHelper
0,
lined_content.size,
surrounding_lines
) if line.include?(query)
) if line.downcase.include?(query.downcase)
end
used_lines.uniq.sort
......
......@@ -16,7 +16,7 @@ class AbuseReport < ActiveRecord::Base
def remove_user(deleted_by:)
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
def notify
......
......@@ -26,7 +26,7 @@ class Repository
#
# For example, for entry `:readme` there's a method called `readme` which
# 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
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref).freeze
......@@ -39,7 +39,6 @@ class Repository
changelog: :changelog,
license: %i(license_blob license_key),
contributing: :contribution_guide,
version: :version,
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
......@@ -116,7 +115,7 @@ class Repository
offset: offset,
after: after,
before: before,
follow: path.present?,
follow: Array(path).length == 1,
skip_merges: skip_merges
}
......@@ -537,11 +536,6 @@ class Repository
end
cache_method :readme
def version
file_on_head(:version)
end
cache_method :version
def contribution_guide
file_on_head(:contributing)
end
......
......@@ -3,9 +3,9 @@ class SpamLog < ActiveRecord::Base
validates :user, presence: true
def remove_user
def remove_user(deleted_by:)
user.block
user.destroy
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true)
end
def text
......
......@@ -59,7 +59,6 @@ module Projects
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
end
def import_data
......
......@@ -26,7 +26,7 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
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
namespace = user.namespace
......
......@@ -318,3 +318,11 @@
%td.shortcut
.key l
%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 @@
#tree-holder.tree-holder
= render 'blob', blob: @blob
- if can_edit_blob?(@blob)
- if can_modify_blob?(@blob)
= render 'projects/blob/remove'
- title = "Replace #{@blob.name}"
......
......@@ -73,11 +73,11 @@
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
.js-truncated-info.truncated-info.hidden
%span<
Showing last
%span.js-truncated-info-size><
KiB of log
.js-truncated-info.truncated-info.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
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
.build-loader-animation.js-build-refresh
......
......@@ -4,7 +4,7 @@
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- 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)
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
......
......@@ -79,4 +79,5 @@
= render 'shared/issuable/sidebar', issuable: @issue
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('issue_show')
......@@ -12,19 +12,21 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
%a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
= link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
.note-headline-light
%span.hidden-xs
= note.author.to_reference
- unless note.system
commented
- if note.system
%span.system-note-message
= note.redacted_note_html
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-header-info
%a{ href: user_path(note.author) }
%span.hidden-xs
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- unless note.system
commented
- if note.system
%span.system-note-message
= 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?
.note-actions
- 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
Edit Snippet
......
- page_title @snippet.title, "Snippets"
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
......
......@@ -14,7 +14,7 @@
%tr
%td.variable-key= variable.key
%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
%span.sr-only
Update
......
......@@ -5,5 +5,5 @@
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page history
- 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
......@@ -3,6 +3,8 @@
= confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
- if issue.closed?
%span.label.label-danger.prepend-left-5 Closed
.pull-right ##{issue.iid}
- if issue.description.present?
.description.term
......@@ -10,6 +12,3 @@
= search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
.pull-right
%span.label.label-danger Closed
......@@ -2,6 +2,10 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%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
- if merge_request.description.present?
.description.term
......@@ -9,8 +13,3 @@
= search_md_sanitize(merge_request, :description)
%span.light
#{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 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
= dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown
- if type != :boards_modal && type != :boards
= dropdown_tag(content_tag(:i, '', class: 'fa fa-history'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
.js-filtered-search-history-dropdown
.filtered-search-box-input-container
.scroll-container
%ul.tokens-container.list-unstyled
......
- page_title "Edit", @snippet.title, "Snippets"
- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
Edit Snippet
%hr
......
- page_title @snippet.title, "Snippets"
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= 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
# 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
# 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
config.assets.digest = false
......
......@@ -42,29 +42,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
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
member do
get 'raw'
......@@ -142,12 +119,6 @@ constraints(ProjectUrlConstrainer.new) do
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
resources :path_locks, only: [:index, :destroy] do
collection do
......@@ -159,16 +130,6 @@ constraints(ProjectUrlConstrainer.new) do
get '/service_desk' => 'service_desk#show', as: :service_desk
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 :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do
......
# All routing related to repositoty browsing
# All routing related to repository browsing
resource :repository, only: [:create] do
member do
......@@ -6,83 +6,91 @@ resource :repository, only: [:create] do
end
end
resources :refs, only: [] do
collection do
get 'switch'
# 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
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
member do
# tree viewer logs
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' => 'refs#logs_tree', as: :logs_file, constraints: {
id: /.*/,
path: /.*/
}
resources :refs, only: [] do
collection do
get 'switch'
end
member do
# tree viewer logs
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
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
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
scope constraints: { id: Gitlab::Regex.git_reference_regex } do
resources :network, only: [:show]
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
# File/dir history
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /.+/, format: false },
as: :commits
)
resources :graphs, only: [:show] do
member do
get :charts
get :commits
get :ci
get :languages
end
end
resources :branches, only: [:index, :new, :create, :destroy]
delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy] do
resource :release, only: [:edit, :update]
end
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]
end
scope constraints: { id: /.+/ } do
scope controller: :blob do
get '/new/*id', action: :new, as: :new_blob
post '/create/*id', action: :create, as: :create_blob
get '/edit/*id', action: :edit, as: :edit_blob
put '/update/*id', action: :update, as: :update_blob
post '/preview/*id', action: :preview, as: :preview_blob
scope path: '/blob/*id', as: :blob do
get :diff
get '/', action: :show
delete '/', action: :destroy
post '/', action: :create
put '/', action: :update
end
end
get '/tree/*id', to: 'tree#show', as: :tree
get '/raw/*id', to: 'raw#show', as: :raw
get '/blame/*id', to: 'blame#show', as: :blame
get '/commits/*id', to: 'commits#show', 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 = {
'pdf_viewer',
'pipelines',
'mr_widget_ee',
'issue_show'
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
......
......@@ -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
- [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
- [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
through courses and videos.
......
......@@ -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,
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
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
still be able to import their GitHub repositories with a
[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:
- the repository description (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,
if you have the privileges to do so.
[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-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 + ?'
| <kbd>r</kbd> | Reply (quoting selected text) |
| <kbd>e</kbd> | Edit issue/merge request |
| <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
end
step 'I search "hand"' do
fill_in 'emoji_search', with: 'hand'
fill_in 'emoji-menu-search', with: 'hand'
end
step 'I see search result for "hand"' do
......@@ -101,7 +101,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
step 'The search field is focused' do
expect(page).to have_selector('#emoji_search')
expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
expect(page).to have_selector('.js-emoji-menu-search')
expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
end
end
......@@ -89,12 +89,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
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
select 'fix', from: 'project_default_branch'
click_button 'Save changes'
......
......@@ -136,7 +136,8 @@ module Banzai
nodes.each_with_object({}) do |node, hash|
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
......
......@@ -172,7 +172,7 @@ module Ci
close_open_tags()
OpenStruct.new(
html: @out,
html: @out.force_encoding(Encoding.default_external),
state: state,
append: append,
truncated: truncated,
......
......@@ -4,7 +4,7 @@ module Gitlab
# This was inspired from: http://stackoverflow.com/a/10219411/1520132
class Stream
BUFFER_SIZE = 4096
LIMIT_SIZE = 50.kilobytes
LIMIT_SIZE = 500.kilobytes
attr_reader :stream
......@@ -14,6 +14,7 @@ module Gitlab
def initialize
@stream = yield
@stream&.binmode
end
def valid?
......@@ -51,7 +52,7 @@ module Gitlab
read_last_lines(last_lines)
else
stream.read
end
end.force_encoding(Encoding.default_external)
end
def html_with_state(state = nil)
......@@ -60,8 +61,8 @@ module Gitlab
def html(last_lines: nil)
text = raw(last_lines: last_lines)
stream = StringIO.new(text)
::Ci::Ansi2html.convert(stream).html
buffer = StringIO.new(text)
::Ci::Ansi2html.convert(buffer).html
end
def extract_coverage(regex)
......@@ -113,7 +114,6 @@ module Gitlab
end
chunks.join.lines.last(last_lines).join
.force_encoding(Encoding.default_external)
end
end
end
......
......@@ -8,7 +8,7 @@ module Gitlab
# the user. We load as much as we can for encoding detection
# (Linguist) and LFS pointer parsing. All other cases where we need full
# 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
......@@ -153,7 +153,7 @@ module Gitlab
def lfs_size
if has_lfs_version_key?
size = data.match(/(?<=size )([0-9]+)/)
return size[1] if size
return size[1].to_i if size
end
nil
......
......@@ -52,7 +52,6 @@ class NewImporter < ::Gitlab::GithubImport::Importer
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
rescue => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
......
......@@ -633,10 +633,10 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
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')
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
......
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
This diff is collapsed.
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
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
......@@ -65,7 +65,7 @@ require('~/lib/utils/common_utils');
$emojiMenu = $('.emoji-menu');
expect($emojiMenu.length).toBe(1);
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);
});
});
......@@ -217,16 +217,35 @@ require('~/lib/utils/common_utils');
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
describe('search', function() {
return it('should filter the emoji', function(done) {
describe('::searchEmojis', () => {
it('should filter the emoji', function(done) {
return openAndWaitForEmojiMenu()
.then(() => {
expect($('[data-name=angel]').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=anger]').is(':visible')).toBe(false);
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)
.catch((err) => {
......@@ -234,6 +253,7 @@ require('~/lib/utils/common_utils');
});
});
});
describe('emoji menu', function() {
const emojiSelector = '[data-name="sunglasses"]';
const openEmojiMenuAndAddEmoji = function() {
......
......@@ -107,4 +107,44 @@ describe('List model', () => {
expect(gl.boardService.moveIssue)
.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 */
/* global Build */
require('~/lib/utils/datetime_utility');
require('~/lib/utils/url_utility');
require('~/build');
require('~/breakpoints');
require('vendor/jquery.nicescroll');
import { bytesToKiB } from '~/lib/utils/number_utils';
import '~/lib/utils/datetime_utility';
import '~/lib/utils/url_utility';
import '~/build';
import '~/breakpoints';
import 'vendor/jquery.nicescroll';
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
......@@ -144,24 +144,6 @@ describe('Build', () => {
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', () => {
spyOn(gl.utils, 'visitUrl');
......@@ -176,6 +158,107 @@ describe('Build', () => {
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 issueTitle from '~/issue_show/issue_title';
import issueTitle from '~/issue_show/issue_title.vue';
describe('Issue Title', () => {
let IssueTitleComponent;
......
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import { formatRelevantDigits, bytesToKiB } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
......@@ -38,4 +38,11 @@ describe('Number Utils', () => {
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
expect(hash).to eq({ link => user })
end
it 'returns an empty Hash when the list of nodes is empty' do
expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
it 'returns an empty Hash when entry does not exist in the database' do
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
......
......@@ -43,24 +43,48 @@ describe Gitlab::Ci::Trace::Stream do
it 'forwards to the next linefeed, case 1' do
stream.limit(7)
expect(stream.raw).to eq('')
result = stream.raw
expect(result).to eq('')
expect(result.encoding).to eq(Encoding.default_external)
end
it 'forwards to the next linefeed, case 2' do
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
describe '#append' do
let(:tempfile) { Tempfile.new }
let(:stream) do
described_class.new do
StringIO.new("12345678")
tempfile.write("12345678")
tempfile.rewind
tempfile
end
end
after do
tempfile.unlink
end
it "truncates and append content" do
stream.append("89", 4)
stream.seek(0)
......@@ -68,6 +92,17 @@ describe Gitlab::Ci::Trace::Stream do
expect(stream.size).to eq(6)
expect(stream.raw).to eq("123489")
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
describe '#set' 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_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.name).to eq("image.jpg") }
it { expect(blob.path).to eq("files/lfs/image.jpg") }
......@@ -273,7 +273,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
it { expect(blob.lfs_pointer?).to eq(false) }
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.name).to eq("picture-invalid.png") }
it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
......
......@@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do
it 'lets a worker delete the user' do
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)
end
......
......@@ -171,6 +171,27 @@ describe Repository, models: true do
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
it 'returns commits with messages containing a given string' do
commit_ids = repository.find_commits_by_message('submodule').map(&:id)
......@@ -1285,7 +1306,6 @@ describe Repository, models: true do
:changelog,
:license,
:contributing,
:version,
:gitignore,
:koding,
:gitlab_ci,
......
require 'spec_helper'
describe SpamLog, models: true do
let(:admin) { create(:admin) }
describe 'associations' do
it { is_expected.to belong_to(:user) }
end
......@@ -13,13 +15,18 @@ describe SpamLog, models: true do
it 'blocks the user' do
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
it 'removes the user' do
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
......@@ -484,7 +484,7 @@ describe 'project routing' do
end
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
......
......@@ -54,6 +54,15 @@ describe Projects::ImportService, services: true do
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"
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
context 'with a non Github repository' do
......
......@@ -152,6 +152,12 @@ describe Users::DestroyService, services: true do
service.execute(user)
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
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