Commit 5fbadf3d authored by Rubén Dávila Santos's avatar Rubén Dávila Santos

Merge branch 'ce-to-ee' into 'master'

CE upstream



See merge request !720
parents 88e44726 a1c6d504
......@@ -84,7 +84,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- knapsack rspec
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
paths:
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.11.2 (unreleased)
v 8.11.1
- Fix file links on project page when default view is Files !5933
- Fixed enter key in search input not working !5888
v 8.12.0 (unreleased)
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Add ability to fork to a specific namespace using API. (ritave)
- Cleanup misalignments in Issue list view !6206
- Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Filter tags by name !6121
- Make push events have equal vertical spacing.
......@@ -16,22 +14,28 @@ v 8.12.0 (unreleased)
- Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
- Fix pagination on user snippets page
- Escape search term before passing it to Regexp.new !6241 (winniehell)
- Fix pinned sidebar behavior in smaller viewports !6169
- Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Center build stage columns in pipeline overview (ClemMakesApps)
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
- Remove suggested colors hover underline (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps)
- Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps)
- Add textarea autoresize after comment (ClemMakesApps)
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
- Fix markdown anchor icon interaction (ClemMakesApps)
- Test migration paths from 8.5 until current release !4874
- Replace animateEmoji timeout with eventListener (ClemMakesApps)
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import
......@@ -40,6 +44,7 @@ v 8.12.0 (unreleased)
- Add Sentry logging to API calls
- Add BroadcastMessage API
- Use 'git update-ref' for safer web commits !6130
- Sort pipelines requested through the API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
......@@ -59,6 +64,7 @@ v 8.12.0 (unreleased)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
- Align add button on repository view (ClemMakesApps)
- Fix contributions calendar month label truncation (ClemMakesApps)
- Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
......@@ -67,6 +73,7 @@ v 8.12.0 (unreleased)
- Fix badge count alignment (ClemMakesApps)
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps)
- Change update interval of contacted_at
- Fix branch title trailing space on hover (ClemMakesApps)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
......@@ -96,13 +103,21 @@ v 8.12.0 (unreleased)
- Refactor the triggers page and documentation !6217
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
- Use default clone protocol on "check out, review, and merge locally" help page URL
- API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
v 8.11.5 (unreleased)
- Optimize branch lookups and force a repository reload for Repository#find_branch
- Fix member expiration date picker after update
v 8.11.6 (unreleased)
v 8.11.5
- Optimize branch lookups and force a repository reload for Repository#find_branch. !6087
- Fix member expiration date picker after update. !6184
- Fix suggested colors options for new labels in the admin area. !6138
- Optimize discussion notes resolving and unresolving
- Fix GitLab import button
- Fix confidential issues being exposed as public using gitlab.com export
- Remove gitorious from import_sources. !6180
- Scope webhooks/services that will run for confidential issues
- Remove gitorious from import_sources
- Fix confidential issues being exposed as public using gitlab.com export
v 8.11.4
- Fix resolving conflicts on forks. !6082
......@@ -116,13 +131,10 @@ v 8.11.4
- Creating an issue through our API now emails label subscribers !5720
- Block concurrent updates for Pipeline
- Don't create groups for unallowed users when importing projects
- Fix resolving conflicts on forks
- Fix diff commenting on merge requests created prior to 8.10
- Don't create groups for unallowed users when importing projects
- Scope webhooks/services that will run for confidential issues
- Fix issue boards leak private label names and descriptions
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Remove gitorious. !5866
- Allow compare merge request versions
v 8.11.3
- Allow system info page to handle case where info is unavailable
......@@ -135,6 +147,7 @@ v 8.11.3
- Fix external issue tracker "Issues" link leading to 404s
- Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Issues filters reset button
v 8.11.2
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
......
......@@ -608,7 +608,7 @@ GEM
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.5)
rouge (2.0.6)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......
......@@ -177,9 +177,7 @@
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
var $el;
$el = $(el);
return $el.data('placement') || 'bottom';
return $(el).data('placement') || 'bottom';
}
});
$('.trigger-submit').on('change', function() {
......@@ -292,42 +290,9 @@
gl.awardsHandler = new AwardsHandler();
checkInitialSidebarSize();
new Aside();
if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
$.cookie('pin_nav', 'false', {
path: gon.relative_url_root || '/',
expires: 365 * 10
});
$('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
$('.navbar-fixed-top').removeClass('header-pinned-nav');
}
$document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText;
e.preventDefault();
$pinBtn = $(e.currentTarget);
$page = $('.page-with-sidebar');
$topNav = $('.navbar-fixed-top');
$tooltip = $("#" + ($pinBtn.attr('aria-describedby')));
doPinNav = !$page.is('.page-sidebar-pinned');
tooltipText = 'Pin navigation';
$(this).toggleClass('is-active');
if (doPinNav) {
$page.addClass('page-sidebar-pinned');
$topNav.addClass('header-pinned-nav');
} else {
$tooltip.remove();
$page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
$topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
}
$.cookie('pin_nav', doPinNav, {
path: gon.relative_url_root || '/',
expires: 365 * 10
});
if ($.cookie('pin_nav') === 'true' || doPinNav) {
tooltipText = 'Unpin navigation';
}
$tooltip.find('.tooltip-inner').text(tooltipText);
return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
});
// bind sidebar events
new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
......
......@@ -255,12 +255,12 @@
};
AwardsHandler.prototype.animateEmoji = function($emoji) {
var className;
className = 'pulse animated';
var className = 'pulse animated once short';
$emoji.addClass(className);
return setTimeout((function() {
return $emoji.removeClass(className);
}), 321);
$emoji.on('webkitAnimationEnd animationEnd', function() {
$(this).removeClass(className);
});
};
AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
......
......@@ -8,6 +8,7 @@
Issuable.initTemplates();
Issuable.initSearch();
Issuable.initChecks();
Issuable.initResetFilters();
return Issuable.initLabelFilterRemove();
},
initTemplates: function() {
......@@ -55,6 +56,17 @@
return Turbolinks.visit(issuesUrl);
};
})(this),
initResetFilters: function() {
$('.reset-filters').on('click', function(e) {
e.preventDefault();
const target = e.target;
const $form = $(target).parents('.js-filter-form');
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
Turbolinks.visit(baseIssuesUrl);
});
},
initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() {
......@@ -64,19 +76,22 @@
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
},
checkChanged: function() {
var checked_issues, ids;
checked_issues = $('.selected_issue:checked');
if (checked_issues.length > 0) {
ids = $.map(checked_issues, function(value) {
const $checkedIssues = $('.selected_issue:checked');
const $updateIssuesIds = $('#update_issues_ids');
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
$('#update_issues_ids').val(ids);
$('.issues-other-filters').hide();
$('.issues_bulk_update').show();
$updateIssuesIds.val(ids);
$issuesOtherFilters.hide();
$issuesBulkUpdate.show();
} else {
$('#update_issues_ids').val([]);
$('.issues_bulk_update').hide();
$('.issues-other-filters').show();
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
this.issuableBulkActions.willUpdateLabels = false;
}
return true;
......
......@@ -331,7 +331,12 @@
form.find(".js-md-write-button").click();
form.find(".js-note-text").val("").trigger("input");
form.find(".js-note-text").data("autosave").reset();
return this.updateTargetButtons(e);
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
form.find('.js-autosize')[0].dispatchEvent(event);
this.updateTargetButtons(e);
};
Notes.prototype.reenableTargetFormSubmitButton = function() {
......
(function() {
var collapsed, expanded, toggleSidebar;
collapsed = 'page-sidebar-collapsed';
expanded = 'page-sidebar-expanded';
toggleSidebar = function() {
$('.page-with-sidebar').toggleClass(collapsed + " " + expanded);
$('.navbar-fixed-top').toggleClass("header-collapsed header-expanded");
if ($.cookie('pin_nav') === 'true') {
$('.navbar-fixed-top').toggleClass('header-pinned-nav');
$('.page-with-sidebar').toggleClass('page-sidebar-pinned');
}
return setTimeout((function() {
var niceScrollBars;
niceScrollBars = $('.nav-sidebar').niceScroll();
return niceScrollBars.updateScrollBar();
}), 300);
};
$(document).off('click', 'body').on('click', 'body', function(e) {
var $nav, $target, $toggle, pageExpanded;
if ($.cookie('pin_nav') !== 'true') {
$target = $(e.target);
$nav = $target.closest('.sidebar-wrapper');
pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded');
$toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle');
if ($nav.length === 0 && pageExpanded && $toggle.length === 0) {
$('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded');
}
}
});
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) {
e.preventDefault();
return toggleSidebar();
});
}).call(this);
((global) => {
let singleton;
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
const navbarSelector = '.navbar-fixed-top';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
const pinnedToggleSelector = '.js-nav-pin';
const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
const pinnedPageClass = 'page-sidebar-pinned';
const expandedPageClass = 'page-sidebar-expanded';
const pinnedNavbarClass = 'header-sidebar-pinned';
const expandedNavbarClass = 'header-sidebar-expanded';
class Sidebar {
constructor() {
if (!singleton) {
singleton = this;
singleton.init();
}
return singleton;
}
init() {
this.isPinned = $.cookie(pinnedStateCookie) === 'true';
this.isExpanded = (
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body', (e) => this.handleClickEvent(e))
.on('page:change', () => this.renderState());
this.renderState();
}
handleClickEvent(e) {
if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
const $target = $(e.target);
const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
this.toggleSidebar();
}
}
}
toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.renderState();
}
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
this.isExpanded = false;
}
$.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', {
path: gon.relative_url_root || '/',
expires: 3650
});
this.renderState();
}
renderState() {
$(pageSelector)
.toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
.toggleClass(expandedPageClass, this.isExpanded);
$(navbarSelector)
.toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
.toggleClass(expandedNavbarClass, this.isExpanded);
const $pinnedToggle = $(pinnedToggleSelector);
const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
}
}
}
global.Sidebar = Sidebar;
})(window.gl || (window.gl = {}));
......@@ -52,8 +52,22 @@
this.initTooltips();
}
// Add extra padding for the last month label if it is also the last column
Calendar.prototype.getExtraWidthPadding = function(group) {
var extraWidthPadding = 0;
var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
if (lastColMonth != secondLastColMonth) {
extraWidthPadding = 3;
}
return extraWidthPadding;
}
Calendar.prototype.renderSvg = function(group) {
return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar');
var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
};
Calendar.prototype.renderDays = function() {
......
......@@ -8,65 +8,44 @@
// Copyright (c) 2016 Daniel Eden
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@include webkit-prefix(animation-duration, 1s);
@include webkit-prefix(animation-fill-mode, both);
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
&.infinite {
@include webkit-prefix(animation-iteration-count, infinite);
}
.animated.flipOutX,
.animated.flipOutY,
.animated.bounceIn,
.animated.bounceOut {
-webkit-animation-duration: .75s;
animation-duration: .75s;
}
&.once {
@include webkit-prefix(animation-iteration-count, 1);
}
@-webkit-keyframes pulse {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
&.hinge {
@include webkit-prefix(animation-duration, 2s);
}
50% {
-webkit-transform: scale3d(1.05, 1.05, 1.05);
transform: scale3d(1.05, 1.05, 1.05);
&.flipOutX,
&.flipOutY,
&.bounceIn,
&.bounceOut {
@include webkit-prefix(animation-duration, .75s);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
&.short {
@include webkit-prefix(animation-duration, 321ms);
@include webkit-prefix(animation-fill-mode, none);
}
}
@keyframes pulse {
from {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
@include keyframes(pulse) {
from, to {
@include webkit-prefix(transform, scale3d(1, 1, 1));
}
50% {
-webkit-transform: scale3d(1.05, 1.05, 1.05);
transform: scale3d(1.05, 1.05, 1.05);
}
to {
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
@include webkit-prefix(transform, scale3d(1.05, 1.05, 1.05));
}
}
.pulse {
-webkit-animation-name: pulse;
animation-name: pulse;
@include webkit-prefix(animation-name, pulse);
}
......@@ -249,6 +249,10 @@
> .controls {
float: right;
}
.new-branch {
margin-top: 3px;
}
}
.content-block-small {
......
......@@ -63,7 +63,7 @@
&.image_file {
background: #eee;
text-align: center;
img {
padding: 20px;
max-width: 80%;
......@@ -94,7 +94,6 @@
&.blame {
table {
border: none;
box-shadow: none;
margin: 0;
}
tr {
......@@ -108,19 +107,10 @@
border-right: none;
}
}
img.avatar {
border: 0 none;
float: none;
margin: 0;
padding: 0;
}
td.blame-commit {
padding: 0 10px;
min-width: 400px;
background: $gray-light;
min-width: 350px;
.commit-author-link {
color: #888;
}
}
td.line-numbers {
float: none;
......@@ -133,12 +123,6 @@
}
td.lines {
padding: 0;
code {
font-family: $monospace_font;
}
pre {
margin: 0;
}
}
}
......
.filter-item {
margin-right: 6px;
vertical-align: top;
&.reset-filters {
padding: 7px;
}
}
@media (min-width: $screen-sm-min) {
......
......@@ -77,10 +77,6 @@ header {
}
}
&.header-collapsed {
padding: 0 16px;
}
.side-nav-toggle {
position: absolute;
left: -10px;
......
......@@ -85,3 +85,13 @@
#{'-webkit-' + $property}: $value;
#{$property}: $value;
}
@mixin keyframes($animation-name) {
@-webkit-keyframes #{$animation-name} {
@content;
}
@keyframes #{$animation-name} {
@content;
}
}
.page-with-sidebar {
padding-top: $header-height;
padding-bottom: 25px;
padding: $header-height 0 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
......@@ -15,6 +14,7 @@
bottom: 0;
left: 0;
height: 100%;
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
......@@ -128,10 +128,8 @@
.fa {
transition: transform .15s;
}
&.is-active {
.fa {
.page-sidebar-pinned & {
transform: rotate(90deg);
}
}
......@@ -152,14 +150,6 @@
}
}
.page-sidebar-collapsed {
padding-left: 0;
.sidebar-wrapper {
width: 0;
}
}
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
......@@ -175,7 +165,7 @@
}
}
header.header-pinned-nav {
header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
......
......@@ -93,11 +93,8 @@
}
.award-control {
margin-right: 5px;
margin-bottom: 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
margin: 3px 5px 3px 0;
padding: 6px 5px;
outline: 0;
&:hover,
......
......@@ -48,12 +48,6 @@
margin-bottom: 10px;
}
}
.page-sidebar-collapsed {
.scroll-controls {
left: 70px;
}
}
}
.build-header {
......
......@@ -375,7 +375,7 @@
}
}
.mr-version-switch {
.mr-version-controls {
background: $background-color;
padding: $gl-btn-padding;
color: $gl-placeholder-color;
......
......@@ -490,6 +490,6 @@
.ci-status-icon-created {
svg {
fill: $table-text-gray;
fill: $gray-darkest;
}
}
......@@ -7,19 +7,14 @@ module Ci
def create
@content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content)
@status = @error.blank?
if @content.blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@status = true
end
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message
@status = false
rescue
@error = 'Undefined error'
@status = false
......
......@@ -91,16 +91,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
render_404
end
end
respond_to do |format|
format.html { define_discussion_vars }
format.json do
unless @merge_request_diff.latest?
# Disable comments if browsing older version of the diff
@diff_notes_disabled = true
if @start_sha
compared_diff_version
else
original_diff_version
end
@diffs = @merge_request_diff.diffs(diff_options)
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
end
......@@ -588,4 +599,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
def compared_diff_version
@diff_notes_disabled = true
@diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
end
def original_diff_version
@diff_notes_disabled = !@merge_request_diff.latest?
@diffs = @merge_request_diff.diffs(diff_options)
end
end
......@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
all_pipelines = project.pipelines
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count
end
def new
......
class PipelinesFinder
attr_reader :project
attr_reader :project, :pipelines
def initialize(project)
@project = project
@pipelines = project.pipelines
end
def execute(pipelines, scope)
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(pipelines, ids_for_ref(pipelines, branches))
when 'tags'
from_ids(pipelines, ids_for_ref(pipelines, tags))
else
pipelines
end
def execute(scope: nil)
scoped_pipelines =
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
pipelines
end
scoped_pipelines.order(id: :desc)
end
private
def ids_for_ref(pipelines, refs)
def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(pipelines, ids)
def from_ids(ids)
pipelines.unscoped.where(id: ids)
end
......
......@@ -14,7 +14,8 @@ module AvatarsHelper
avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}",
alt: "#{user_name}'s avatar",
title: user_name
title: user_name,
data: { container: 'body' }
)
if options[:user]
......
......@@ -2,4 +2,8 @@ module GitHelper
def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
end
def short_sha(text)
Commit.truncate_sha(text)
end
end
......@@ -137,4 +137,14 @@ module MergeRequestsHelper
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
diffs_namespace_project_merge_request_path(
project.namespace, project, merge_request,
diff_id: merge_request_diff.id, start_sha: start_sha)
end
def version_index(merge_request_diff)
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
end
end
module NavHelper
def nav_menu_collapsed?
cookies[:collapsed_nav] == 'true'
end
def nav_sidebar_class
if nav_menu_collapsed?
"sidebar-collapsed"
else
"sidebar-expanded"
end
end
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
else
"page-sidebar-collapsed"
end
end
......@@ -26,7 +12,6 @@ module NavHelper
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
......@@ -43,9 +28,7 @@ module NavHelper
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-expanded header-pinned-nav"
else
class_name << " header-collapsed"
class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name
......
......@@ -129,6 +129,19 @@ module ProjectsHelper
current_user.recent_push(project_ids)
end
def project_feature_access_select(field)
# Don't show option "everyone with access" if project is private
options = project_feature_options
if @project.private?
options.delete('Everyone with access')
highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
end
private
def get_project_nav_tabs(project, current_user)
......@@ -439,15 +452,4 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
def project_feature_access_select(field)
# Don't show option "everyone with access" if project is private
options = project_feature_options
level = @project.project_feature.public_send(field)
options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
end
end
......@@ -7,8 +7,10 @@ module SearchHelper
projects_autocomplete(term)
].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i")
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
generic_results.select! { |result| result[:label] =~ search_pattern }
[
resources_results,
......
......@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago
LAST_CONTACT_TIME = 2.hours.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
......
......@@ -13,6 +13,11 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
# Keep this scope in sync with the logic in `#resolvable?`
scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
scope :unresolved, -> { resolvable.where(resolved_at: nil) }
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
......@@ -25,6 +30,16 @@ class DiffNote < Note
def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-")
end
# This method must be kept in sync with `#resolve!`
def resolve!(current_user)
unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
end
# This method must be kept in sync with `#unresolve!`
def unresolve!
resolved.update_all(resolved_at: nil, resolved_by_id: nil)
end
end
def new_diff_note?
......@@ -73,6 +88,7 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
# If you update this method remember to also update the scope `resolvable`
def resolvable?
!system? && for_merge_request?
end
......@@ -83,6 +99,7 @@ class DiffNote < Note
self.resolved_at.present?
end
# If you update this method remember to also update `.resolve!`
def resolve!(current_user)
return unless resolvable?
return if resolved?
......@@ -92,6 +109,7 @@ class DiffNote < Note
save!
end
# If you update this method remember to also update `.unresolve!`
def unresolve!
return unless resolvable?
return unless resolved?
......
class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
attr_reader :first_note, :last_note, :notes
attr_reader :notes
delegate :created_at,
:project,
......@@ -36,8 +36,6 @@ class Discussion
end
def initialize(notes)
@first_note = notes.first
@last_note = notes.last
@notes = notes
end
......@@ -70,17 +68,25 @@ class Discussion
end
def resolvable?
return @resolvable if defined?(@resolvable)
return @resolvable if @resolvable.present?
@resolvable = diff_discussion? && notes.any?(&:resolvable?)
end
def resolved?
return @resolved if defined?(@resolved)
return @resolved if @resolved.present?
@resolved = resolvable? && notes.none?(&:to_be_resolved?)
end
def first_note
@first_note ||= @notes.first
end
def last_note
@last_note ||= @notes.last
end
def resolved_notes
notes.select(&:resolved?)
end
......@@ -100,17 +106,13 @@ class Discussion
def resolve!(current_user)
return unless resolvable?
notes.each do |note|
note.resolve!(current_user) if note.resolvable?
end
update { |notes| notes.resolve!(current_user) }
end
def unresolve!
return unless resolvable?
notes.each do |note|
note.unresolve! if note.resolvable?
end
update { |notes| notes.unresolve! }
end
def for_target?(target)
......@@ -118,7 +120,7 @@ class Discussion
end
def active?
return @active if defined?(@active)
return @active if @active.present?
@active = first_note.active?
end
......@@ -174,4 +176,17 @@ class Discussion
prev_lines
end
private
def update
notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
yield(notes_relation)
# Set the notes array to the updated notes
@notes = notes_relation.to_a
# Reset the memoized values
@last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
end
end
......@@ -152,6 +152,10 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
def compare_with(sha)
CompareService.new.execute(project, head_commit_sha, project, sha)
end
private
def dump_commits(commits)
......
......@@ -847,7 +847,7 @@ class Repository
end
def commit_dir(user, path, message, branch)
commit_with_hooks(user, branch) do |ref|
update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
......@@ -864,7 +864,7 @@ class Repository
end
def commit_file(user, path, content, message, branch, update)
commit_with_hooks(user, branch) do |ref|
update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
......@@ -886,7 +886,7 @@ class Repository
end
def update_file(user, path, content, branch:, previous_path:, message:)
commit_with_hooks(user, branch) do |ref|
update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
......@@ -913,7 +913,7 @@ class Repository
end
def remove_file(user, path, message, branch)
commit_with_hooks(user, branch) do |ref|
update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
......@@ -963,7 +963,7 @@ class Repository
raise "Invalid merge target" if our_commit.nil?
raise "Invalid merge source" if their_commit.nil?
commit_with_hooks(user, target_branch) do
update_branch_with_hooks(user, target_branch) do
merge_request.update(in_progress_merge_commit_sha: their_commit.oid) if merge_request
their_commit.oid
end
......@@ -979,7 +979,7 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
commit_with_hooks(user, merge_request.target_branch) do
update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
......@@ -997,7 +997,7 @@ class Repository
return false unless revert_tree_id
commit_with_hooks(user, base_branch) do
update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
......@@ -1014,7 +1014,7 @@ class Repository
return false unless cherry_pick_tree_id
commit_with_hooks(user, base_branch) do
update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.message,
......@@ -1030,7 +1030,7 @@ class Repository
end
def resolve_conflicts(user, branch, params)
commit_with_hooks(user, branch) do
update_branch_with_hooks(user, branch) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
......@@ -1233,7 +1233,7 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
def commit_with_hooks(current_user, branch)
def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
......
......@@ -7,277 +7,278 @@
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
.modal-body.shortcuts-cheatsheet
.col-lg-4
%table.shortcut-mappings
%tbody
%tr
%th
%th Global Shortcuts
%tr
%td.shortcut
.key s
%td Focus Search
%tr
%td.shortcut
.key f
%td Focus Filter
%tr
%td.shortcut
.key ?
%td Show/hide this dialog
%tr
%td.shortcut
- if browser.platform.mac?
.key &#8984; shift p
- else
.key ctrl shift p
%td Toggle Markdown preview
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Edit last comment (when focused on an empty textarea)
%tbody
%tr
%th
%th Project Files browsing
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%tbody
%tr
%th
%th Finding Project File
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%tr
%td.shortcut
.key esc
%td Go back
.modal-body
.row
.col-lg-4
%table.shortcut-mappings
%tbody
%tr
%th
%th Global Shortcuts
%tr
%td.shortcut
.key s
%td Focus Search
%tr
%td.shortcut
.key f
%td Focus Filter
%tr
%td.shortcut
.key ?
%td Show/hide this dialog
%tr
%td.shortcut
- if browser.platform.mac?
.key &#8984; shift p
- else
.key ctrl shift p
%td Toggle Markdown preview
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Edit last comment (when focused on an empty textarea)
%tbody
%tr
%th
%th Project Files browsing
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%tbody
%tr
%th
%th Finding Project File
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
%td Move selection up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
%td Move selection down
%tr
%td.shortcut
.key enter
%td Open Selection
%tr
%td.shortcut
.key esc
%td Go back
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut project', style: 'display:none' }
%tr
%th
%th Global Dashboard
%tr
%td.shortcut
.key g
.key a
%td
Go to the activity feed
%tr
%td.shortcut
.key g
.key p
%td
Go to projects
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tbody
%tr
%th
%th Project
%tr
%td.shortcut
.key g
.key p
%td
Go to the project's home page
%tr
%td.shortcut
.key g
.key e
%td
Go to the project's activity feed
%tr
%td.shortcut
.key g
.key f
%td
Go to files
%tr
%td.shortcut
.key g
.key c
%td
Go to commits
%tr
%td.shortcut
.key g
.key b
%td
Go to builds
%tr
%td.shortcut
.key g
.key n
%td
Go to network graph
%tr
%td.shortcut
.key g
.key g
%td
Go to graphs
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tr
%td.shortcut
.key g
.key s
%td
Go to snippets
%tr
%td.shortcut
.key t
%td Go to finding file
%tr
%td.shortcut
.key i
%td New issue
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut network', style: 'display:none' }
%tr
%th
%th Network Graph
%tr
%td.shortcut
.key
%i.fa.fa-arrow-left
\/
.key h
%td Scroll left
%tr
%td.shortcut
.key
%i.fa.fa-arrow-right
\/
.key l
%td Scroll right
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
\/
.key k
%td Scroll up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
\/
.key j
%td Scroll down
%tr
%td.shortcut
.key
shift
%i.fa.fa-arrow-up
\/
.key
shift k
%td Scroll to top
%tr
%td.shortcut
.key
shift
%i.fa.fa-arrow-down
\/
.key
shift j
%td Scroll to bottom
%tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
%tr
%th
%th Issues
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
%tr
%td.shortcut
.key r
%td Reply (quoting selected text)
%tr
%td.shortcut
.key e
%td Edit issue
%tr
%td.shortcut
.key l
%td Change Label
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
%th Merge Requests
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
%tr
%td.shortcut
.key r
%td Reply (quoting selected text)
%tr
%td.shortcut
.key e
%td Edit merge request
%tr
%td.shortcut
.key l
%td Change Label
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut project', style: 'display:none' }
%tr
%th
%th Global Dashboard
%tr
%td.shortcut
.key g
.key a
%td
Go to the activity feed
%tr
%td.shortcut
.key g
.key p
%td
Go to projects
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tbody
%tr
%th
%th Project
%tr
%td.shortcut
.key g
.key p
%td
Go to the project's home page
%tr
%td.shortcut
.key g
.key e
%td
Go to the project's activity feed
%tr
%td.shortcut
.key g
.key f
%td
Go to files
%tr
%td.shortcut
.key g
.key c
%td
Go to commits
%tr
%td.shortcut
.key g
.key b
%td
Go to builds
%tr
%td.shortcut
.key g
.key n
%td
Go to network graph
%tr
%td.shortcut
.key g
.key g
%td
Go to graphs
%tr
%td.shortcut
.key g
.key i
%td
Go to issues
%tr
%td.shortcut
.key g
.key m
%td
Go to merge requests
%tr
%td.shortcut
.key g
.key s
%td
Go to snippets
%tr
%td.shortcut
.key t
%td Go to finding file
%tr
%td.shortcut
.key i
%td New issue
.col-lg-4
%table.shortcut-mappings
%tbody{ class: 'hidden-shortcut network', style: 'display:none' }
%tr
%th
%th Network Graph
%tr
%td.shortcut
.key
%i.fa.fa-arrow-left
\/
.key h
%td Scroll left
%tr
%td.shortcut
.key
%i.fa.fa-arrow-right
\/
.key l
%td Scroll right
%tr
%td.shortcut
.key
%i.fa.fa-arrow-up
\/
.key k
%td Scroll up
%tr
%td.shortcut
.key
%i.fa.fa-arrow-down
\/
.key j
%td Scroll down
%tr
%td.shortcut
.key
shift
%i.fa.fa-arrow-up
\/
.key
shift k
%td Scroll to top
%tr
%td.shortcut
.key
shift
%i.fa.fa-arrow-down
\/
.key
shift j
%td Scroll to bottom
%tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
%tr
%th
%th Issues
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
%tr
%td.shortcut
.key r
%td Reply (quoting selected text)
%tr
%td.shortcut
.key e
%td Edit issue
%tr
%td.shortcut
.key l
%td Change Label
%tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
%th Merge Requests
%tr
%td.shortcut
.key a
%td Change assignee
%tr
%td.shortcut
.key m
%td Change milestone
%tr
%td.shortcut
.key r
%td Reply (quoting selected text)
%tr
%td.shortcut
.key e
%td Edit merge request
%tr
%td.shortcut
.key l
%td Change Label
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.sidebar-wrapper.nicescroll
.sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation
......
......@@ -11,7 +11,7 @@
%small= number_to_human_size @blob.size
.file-actions
= render "projects/blob/actions"
.file-content.blame.code.js-syntax-highlight
.table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- @blame_groups.each do |blame_group|
......@@ -19,6 +19,7 @@
%td.blame-commit
.commit
- commit = blame_group[:commit]
= author_avatar(commit, size: 36)
.commit-row-title
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
......
- if can?(current_user, :push_code, @project)
.pull-right
#new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
#new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
= link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
= icon('spinner spin')
Checking branches
......
......@@ -83,7 +83,7 @@
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block
.content-block.content-block-small
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row
......
- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
- if merge_request_diffs.size > 1
.mr-version-switch
Version:
%span.dropdown.inline
- if @merge_request_diffs.size > 1
.mr-version-controls
Changes between
%span.dropdown.inline.mr-version-dropdown
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
%strong.monospace<
%strong
- if @merge_request_diff.latest?
#{"latest"}
latest version
- else
#{@merge_request_diff.head_commit.short_id}
version #{version_index(@merge_request_diff)}
%span.caret
%ul.dropdown-menu.dropdown-menu-selectable
- merge_request_diffs.each do |merge_request_diff|
- @merge_request_diffs.each do |merge_request_diff|
%li
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%strong.monospace
#{merge_request_diff.head_commit.short_id}
%br
= link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%strong
- if merge_request_diff.latest?
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
#{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
- unless @merge_request_diff.latest?
%span.prepend-left-default
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
%strong
- if @start_sha
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
%span.caret
%ul.dropdown-menu.dropdown-menu-selectable
- @comparable_diffs.each do |merge_request_diff|
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
%strong
- if merge_request_diff.latest?
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.prepend-top-10
= icon('info-circle')
This version is not the latest one. Comments are disabled
.pull-right
%span.monospace
#{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id}
- if @start_sha
Comments are disabled because you're comparing two versions of this merge request.
- else
Comments are disabled because you're viewing an old version of this merge request.
......@@ -38,6 +38,9 @@
%a{href: "#", data: { id: weight }, class: ("is-active" if params[:weight] == weight.to_s)}
= weight
.filter-item.inline.reset-filters
%a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters
.pull-right
- if controller.controller_name == 'boards'
#js-boards-seach.issue-boards-search
......
class PruneOldEventsWorker
include Sidekiq::Worker
def perform
# Contribution calendar shows maximum 12 months of events.
# Double nested query is used because MySQL doesn't allow DELETE subqueries
# on the same table.
Event.unscoped.where(
'(id IN (SELECT id FROM (?) ids_to_remove))',
Event.unscoped.where(
'created_at < ?',
(12.months + 1.day).ago).
select(:id).
limit(10_000)).
delete_all
end
end
......@@ -390,6 +390,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
#
# GitLab Shell
......
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160901141443) do
ActiveRecord::Schema.define(version: 20160902122721) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -42,8 +42,9 @@ following locations:
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
- [Tags](tags.md)
- [Users](users.md)
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
### Internal CI API
......
# Validate the .gitlab-ci.yml
> [Introduced][ce-5953] in GitLab 8.12.
Checks if your .gitlab-ci.yml file is valid.
```
POST ci/lint
```
| Attribute | Type | Required | Description |
| ---------- | ------- | -------- | -------- |
| `content` | string | yes | the .gitlab-ci.yaml content|
```bash
curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
```
Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
Example responses:
* Valid content:
```json
{
"status": "valid",
"errors": []
}
```
* Invalid content:
```json
{
"status": "invalid",
"errors": [
"variables config should be a hash of key value pairs"
]
}
```
* Without the content attribute:
```json
{
"error": "content is missing"
}
```
......@@ -110,8 +110,8 @@ POST /projects/:id/members
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members
```
Example response:
......
......@@ -518,7 +518,7 @@ invalid, 400 is returned.
### Fork project
Forks a project into the user namespace of the authenticated user.
Forks a project into the user namespace of the authenticated user or the one provided.
```
POST /projects/fork/:id
......@@ -527,6 +527,7 @@ POST /projects/fork/:id
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `namespace` (optional) - The ID or path of the namespace that the project will be forked to
### Star a project
......
......@@ -76,8 +76,8 @@ export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux"
export CI_SERVER="yes"
export CI_SERVER_NAME="GitLab"
export CI_SERVER_REVISION="8.9.0"
export CI_SERVER_VERSION="70606bf"
export CI_SERVER_REVISION="70606bf"
export CI_SERVER_VERSION="8.9.0"
```
### YAML-defined variables
......
......@@ -86,7 +86,7 @@ if your available memory changes.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
## Gitlab Runner
## GitLab Runner
We strongly advise against installing GitLab Runner on the same machine you plan
to install GitLab on. Depending on how you decide to configure GitLab Runner and
......
......@@ -12,6 +12,12 @@ can select an older one from version dropdown.
![Merge Request Versions](img/versions.png)
You can also compare the merge request version with older one to see what is
changed since then.
Please note that comments are disabled while viewing outdated merge versions
or comparing to versions other than base.
---
>**Note:**
......
......@@ -49,6 +49,7 @@ module API
mount ::API::LicenseTemplates
mount ::API::Ldap
mount ::API::LdapGroupLinks
mount ::API::Lint
mount ::API::Members
mount ::API::MergeRequests
mount ::API::Milestones
......
module API
class Lint < Grape::API
namespace :ci do
desc 'Validation of .gitlab-ci.yml content'
params do
requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
end
post '/lint' do
error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
status 200
if error.blank?
{ status: 'valid', errors: [] }
else
{ status: 'invalid', errors: [error] }
end
end
end
end
end
......@@ -13,11 +13,14 @@ module API
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
optional :scope, type: String, values: ['running', 'branches', 'tags'],
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
present paginate(user_project.pipelines), with: Entities::Pipeline
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline
end
desc 'Gets a specific pipeline for the project' do
......
......@@ -193,16 +193,30 @@ module API
end
end
# Fork new project for the current user.
# Fork new project for the current user or provided namespace.
#
# Parameters:
# id (required) - The ID of a project
# namespace (optional) - The ID or name of the namespace that the project will be forked into.
# Example Request
# POST /projects/fork/:id
post 'fork/:id' do
attrs = {}
namespace_id = params[:namespace]
if namespace_id.present?
namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
not_found!('Target Namespace') unless namespace
attrs[:namespace] = namespace
end
@forked_project =
::Projects::ForkService.new(user_project,
current_user).execute
current_user,
attrs).execute
if @forked_project.errors.any?
conflict!(@forked_project.errors.messages)
else
......
......@@ -12,7 +12,7 @@ module Ci
# POST /builds/register
post "register" do
authenticate_runner!
update_runner_last_contact
update_runner_last_contact(save: false)
update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
......
......@@ -3,7 +3,7 @@ module Ci
module Helpers
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 60
UPDATE_RUNNER_EVERY = 40 * 60
def authenticate_runners!
forbidden! unless runner_registration_token_valid?
......@@ -22,11 +22,13 @@ module Ci
params[:token] == current_application_settings.runners_registration_token
end
def update_runner_last_contact
def update_runner_last_contact(save: true)
# Use a random threshold to prevent beating DB updates
# it generates a distribution between: [40m, 80m]
contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
current_runner.update_attributes(contacted_at: Time.now)
current_runner.contacted_at = Time.now
current_runner.save if current_runner.changed? && save
end
end
......
......@@ -55,12 +55,7 @@ module Ci
{
stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
##
# Refactoring note:
# - before script behaves differently than after script
# - after script returns an array of commands
# - before script should be a concatenated command
commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
allow_failure: job[:allow_failure] || false,
......@@ -68,16 +63,27 @@ module Ci
environment: job[:environment],
yaml_variables: yaml_variables(name),
options: {
image: job[:image] || @image,
services: job[:services] || @services,
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
cache: job[:cache],
dependencies: job[:dependencies],
after_script: job[:after_script] || @after_script,
after_script: job[:after_script],
}.compact
}
end
def self.validation_message(content)
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
Ci::GitlabCiYamlProcessor.new(content)
nil
rescue ValidationError, Psych::SyntaxError => e
e.message
end
end
private
def initial_parsing
......
......@@ -21,7 +21,7 @@ module Gitlab
private
def gl_user_id(project, bitbucket_id)
def gitlab_user_id(project, bitbucket_id)
if bitbucket_id
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
(user && user.id) || project.creator_id
......@@ -74,7 +74,7 @@ module Gitlab
description: body,
title: issue["title"],
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
author_id: gl_user_id(project, reporter)
author_id: gitlab_user_id(project, reporter)
)
end
rescue ActiveRecord::RecordInvalid => e
......
......@@ -14,7 +14,7 @@ module Gitlab
@config = Loader.new(config).load!
@global = Node::Global.new(@config)
@global.process!
@global.compose!
end
def valid?
......
......@@ -23,9 +23,9 @@ module Gitlab
end
end
private
def compose!(deps = nil)
return unless valid?
def compose!
self.class.nodes.each do |key, factory|
factory
.value(@config[key])
......@@ -33,6 +33,12 @@ module Gitlab
@entries[key] = factory.create!
end
yield if block_given?
@entries.each_value do |entry|
entry.compose!(deps)
end
end
class_methods do
......
......@@ -20,11 +20,14 @@ module Gitlab
@validator.validate(:new)
end
def process!
def [](key)
@entries[key] || Node::Undefined.new
end
def compose!(deps = nil)
return unless valid?
compose!
descendants.each(&:process!)
yield if block_given?
end
def leaf?
......@@ -73,11 +76,6 @@ module Gitlab
def self.validator
Validator
end
private
def compose!
end
end
end
end
......
......@@ -37,8 +37,8 @@ module Gitlab
# See issue #18775.
#
if @value.nil?
Node::Undefined.new(
fabricate_undefined
Node::Unspecified.new(
fabricate_unspecified
)
else
fabricate(@node, @value)
......@@ -47,13 +47,13 @@ module Gitlab
private
def fabricate_undefined
def fabricate_unspecified
##
# If node has a default value we fabricate concrete node
# with default value.
#
if @node.default.nil?
fabricate(Node::Null)
fabricate(Node::Undefined)
else
fabricate(@node, @node.default)
end
......
......@@ -36,15 +36,15 @@ module Gitlab
helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs
private
def compose!
super
compose_jobs!
compose_deprecated_entries!
def compose!(_deps = nil)
super(self) do
compose_jobs!
compose_deprecated_entries!
end
end
private
def compose_jobs!
factory = Node::Factory.new(Node::Jobs)
.value(@config.except(*self.class.nodes.keys))
......
......@@ -80,7 +80,19 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts
:artifacts, :commands
def compose!(deps = nil)
super do
if type_defined? && !stage_defined?
@entries[:stage] = @entries[:type]
end
@entries.delete(:type)
end
inherit!(deps)
end
def name
@metadata[:name]
......@@ -90,12 +102,30 @@ module Gitlab
@config.merge(to_hash.compact)
end
def commands
(before_script_value.to_a + script_value.to_a).join("\n")
end
private
def inherit!(deps)
return unless deps
self.class.nodes.each_key do |key|
global_entry = deps[key]
job_entry = @entries[key]
if global_entry.specified? && !job_entry.specified?
@entries[key] = global_entry
end
end
end
def to_hash
{ name: name,
before_script: before_script,
script: script,
commands: commands,
image: image,
services: services,
stage: stage,
......@@ -106,16 +136,6 @@ module Gitlab
artifacts: artifacts,
after_script: after_script }
end
def compose!
super
if type_defined? && !stage_defined?
@entries[:stage] = @entries[:type]
end
@entries.delete(:type)
end
end
end
end
......
......@@ -26,19 +26,23 @@ module Gitlab
name.to_s.start_with?('.')
end
private
def compose!
@config.each do |name, config|
node = hidden?(name) ? Node::Hidden : Node::Job
factory = Node::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
description: "#{name} job definition.")
def compose!(deps = nil)
super do
@config.each do |name, config|
node = hidden?(name) ? Node::Hidden : Node::Job
factory = Node::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
description: "#{name} job definition.")
@entries[name] = factory.create!
end
@entries[name] = factory.create!
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
end
......
......@@ -3,15 +3,34 @@ module Gitlab
class Config
module Node
##
# This class represents an unspecified entry node.
# This class represents an undefined node.
#
# It decorates original entry adding method that indicates it is
# unspecified.
# Implements the Null Object pattern.
#
class Undefined < SimpleDelegator
class Undefined < Entry
def initialize(*)
super(nil)
end
def value
nil
end
def valid?
true
end
def errors
[]
end
def specified?
false
end
def relevant?
false
end
end
end
end
......
......@@ -3,30 +3,15 @@ module Gitlab
class Config
module Node
##
# This class represents an undefined node.
# This class represents an unspecified entry node.
#
# Implements the Null Object pattern.
# It decorates original entry adding method that indicates it is
# unspecified.
#
class Null < Entry
def value
nil
end
def valid?
true
end
def errors
[]
end
class Unspecified < SimpleDelegator
def specified?
false
end
def relevant?
false
end
end
end
end
......
......@@ -18,7 +18,7 @@ module Gitlab
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 102400
raise UnmergeableFile if text.length > 200.kilobytes
begin
text.to_json
......
......@@ -15,7 +15,7 @@ module Gitlab
private
def gl_user_id(github_id)
def gitlab_user_id(github_id)
User.joins(:identities).
find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
try(:id)
......
......@@ -21,7 +21,7 @@ module Gitlab
end
def author_id
gl_user_id(raw_data.user.id) || project.creator_id
gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
......
......@@ -40,7 +40,7 @@ module Gitlab
def assignee_id
if assigned?
gl_user_id(raw_data.assignee.id)
gitlab_user_id(raw_data.assignee.id)
end
end
......@@ -49,7 +49,7 @@ module Gitlab
end
def author_id
gl_user_id(raw_data.user.id) || project.creator_id
gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
......
......@@ -68,7 +68,7 @@ module Gitlab
def assignee_id
if assigned?
gl_user_id(raw_data.assignee.id)
gitlab_user_id(raw_data.assignee.id)
end
end
......@@ -77,7 +77,7 @@ module Gitlab
end
def author_id
gl_user_id(raw_data.user.id) || project.creator_id
gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
......
......@@ -41,7 +41,8 @@ module Gitlab
title: issue["title"],
state: issue["state"],
updated_at: issue["updated_at"],
author_id: gl_user_id(project, issue["author"]["id"])
author_id: gitlab_user_id(project, issue["author"]["id"]),
confidential: issue["confidential"]
)
end
end
......@@ -51,7 +52,7 @@ module Gitlab
private
def gl_user_id(project, gitlab_id)
def gitlab_user_id(project, gitlab_id)
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s)
(user && user.id) || project.creator_id
end
......
......@@ -28,6 +28,11 @@ FactoryGirl.define do
diff_refs: noteable.diff_refs
)
end
trait :resolved do
resolved_at { Time.now }
resolved_by { create(:user) }
end
end
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
......
require 'rails_helper'
feature 'Issues filter reset button', feature: true, js: true do
include WaitForAjax
include IssueHelpers
let!(:project) { create(:project, :public) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:bug) { create(:label, project: project, name: 'bug')}
let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
before do
project.team << [user, :developer]
end
context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do
visit_issues(project, milestone_title: milestone.title)
expect(page).to have_css('.issue', count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
context 'when a label filter has been applied' do
it 'resets the label filter' do
visit_issues(project, label_name: bug.name)
expect(page).to have_css('.issue', count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_issues(project, issue_search: 'Bug')
expect(page).to have_css('.issue', count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
context 'when author filter has been applied' do
it 'resets the author filter' do
visit_issues(project, author_id: user.id)
expect(page).to have_css('.issue', count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
visit_issues(project, assignee_id: user.id)
expect(page).to have_css('.issue', count: 1)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
context 'when all filters have been applied' do
it 'resets all filters' do
visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, issue_search: 'Bug')
expect(page).to have_css('.issue', count: 0)
reset_filters
expect(page).to have_css('.issue', count: 2)
end
end
def reset_filters
find('.reset-filters').click
end
end
......@@ -11,8 +11,8 @@ feature 'Merge Request versions', js: true, feature: true do
end
it 'show the latest version of the diff' do
page.within '.mr-version-switch' do
expect(page).to have_content 'Version: latest'
page.within '.mr-version-dropdown' do
expect(page).to have_content 'latest version'
end
expect(page).to have_content '8 changed files'
......@@ -20,18 +20,49 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'switch between versions' do
before do
page.within '.mr-version-switch' do
page.within '.mr-version-dropdown' do
find('.btn-link').click
click_link '6f6d7e7e'
click_link 'version 1'
end
end
it 'should show older version' do
page.within '.mr-version-switch' do
expect(page).to have_content 'Version: 6f6d7e7e'
page.within '.mr-version-dropdown' do
expect(page).to have_content 'version 1'
end
expect(page).to have_content '5 changed files'
end
it 'show the message about disabled comments' do
expect(page).to have_content 'Comments are disabled'
end
end
describe 'compare with older version' do
before do
page.within '.mr-version-compare-dropdown' do
find('.btn-link').click
click_link 'version 1'
end
end
it 'should has correct value in the compare dropdown' do
page.within '.mr-version-compare-dropdown' do
expect(page).to have_content 'version 1'
end
end
it 'show the message about disabled comments' do
expect(page).to have_content 'Comments are disabled'
end
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
end
end
require 'spec_helper'
describe PipelinesFinder do
let(:project) { create(:project) }
let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
subject { described_class.new(project).execute(params) }
describe "#execute" do
context 'when a scope is passed' do
context 'when scope is nil' do
let(:params) { { scope: nil } }
it 'selects all pipelines' do
expect(subject.count).to be 2
expect(subject).to include tag_pipeline
expect(subject).to include branch_pipeline
end
end
context 'when selecting branches' do
let(:params) { { scope: 'branches' } }
it 'excludes tags' do
expect(subject).not_to include tag_pipeline
expect(subject).to include branch_pipeline
end
end
context 'when selecting tags' do
let(:params) { { scope: 'tags' } }
it 'excludes branches' do
expect(subject).to include tag_pipeline
expect(subject).not_to include branch_pipeline
end
end
end
# Scoping to running will speed up the test as it doesn't hit the FS
let(:params) { { scope: 'running' } }
it 'orders in descending order on ID' do
create(:ci_pipeline, project: project, ref: 'feature')
expect(subject.map(&:id)).to eq [3, 2, 1]
end
end
end
require 'spec_helper'
describe GitHelper do
describe '#short_sha' do
let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') }
it { expect(short_sha).to eq('d4e043f6') }
end
end
require 'spec_helper'
# Specs in this file have access to a helper object that includes
# the NavHelper. For example:
#
# describe NavHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
describe NavHelper do
describe '#nav_menu_collapsed?' do
it 'returns true when the nav is collapsed in the cookie' do
helper.request.cookies[:collapsed_nav] = 'true'
expect(helper.nav_menu_collapsed?).to eq true
end
it 'returns false when the nav is not collapsed in the cookie' do
helper.request.cookies[:collapsed_nav] = 'false'
expect(helper.nav_menu_collapsed?).to eq false
end
end
end
......@@ -183,4 +183,48 @@ describe ProjectsHelper do
end
end
end
describe "#project_feature_access_select" do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
context "when project is internal or public" do
it "shows all options" do
helper.instance_variable_set(:@project, project)
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).to include("Everyone with access")
end
end
context "when project is private" do
before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
it "shows only allowed options" do
helper.instance_variable_set(:@project, project)
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
end
end
context "when project moves from public to private" do
before do
project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it "shows the highest allowed level selected" do
helper.instance_variable_set(:@project, project)
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
expect(result).to have_selector('option[selected]', text: "Only team members")
end
end
end
end
......@@ -32,6 +32,10 @@ describe SearchHelper do
expect(search_autocomplete_opts("adm").size).to eq(1)
end
it "does not allow regular expression in search term" do
expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
end
it "includes the user's groups" do
create(:group).add_owner(user)
expect(search_autocomplete_opts("gro").size).to eq(1)
......
.flash-container.timeline-content
.timeline-icon.hidden-xs.hidden-sm
%a.author_link
%img
.timeline-content.timeline-content-form
%form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
.md-area
.md-header
.md-write-holder
.zen-backdrop.div-dropzone-wrapper
.div-dropzone-wrapper
.div-dropzone.dz-clickable
%textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
.note-form-actions.clearfix
%input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
%a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
Reopen issue
%a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
Close issue
%a.btn.btn-cancel.js-note-discard
Discard draft
\ No newline at end of file
/*= require notes */
/*= require autosize */
/*= require gl_form */
/*= require lib/utils/text_utility */
(function() {
window.gon || (window.gon = {});
......@@ -12,29 +11,63 @@
};
describe('Notes', function() {
return describe('task lists', function() {
describe('task lists', function() {
fixture.preload('issue_note.html');
beforeEach(function() {
fixture.load('issue_note.html');
$('form').on('submit', function(e) {
return e.preventDefault();
e.preventDefault();
});
return this.notes = new Notes();
this.notes = new Notes();
});
it('modifies the Markdown field', function() {
$('input[type=checkbox]').attr('checked', true).trigger('change');
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
return it('submits the form on tasklist:changed', function() {
var submitted;
submitted = false;
it('submits the form on tasklist:changed', function() {
var submitted = false;
$('form').on('submit', function(e) {
submitted = true;
return e.preventDefault();
e.preventDefault();
});
$('.js-task-list-field').trigger('tasklist:changed');
return expect(submitted).toBe(true);
expect(submitted).toBe(true);
});
});
describe('comments', function() {
var commentsTemplate = 'comments.html';
var textarea = '.js-note-text';
fixture.preload(commentsTemplate);
beforeEach(function() {
fixture.load(commentsTemplate);
this.notes = new Notes();
this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
spyOn(this.notes, 'renderNote').and.stub();
$(textarea).data('autosave', {
reset: function() {}
});
$('form').on('submit', function(e) {
e.preventDefault();
$('.js-main-target-form').trigger('ajax:success');
});
});
it('autosizes after comment submission', function() {
$(textarea).text('This is an example comment note');
expect(this.autoSizeSpy).not.toHaveBeenTriggered();
$('.js-comment-button').click();
expect(this.autoSizeSpy).toHaveBeenTriggered();
})
});
});
......
......@@ -1250,5 +1250,40 @@ EOT
end
end
end
describe "#validation_message" do
context "when the YAML could not be parsed" do
it "returns an error about invalid configutaion" do
content = YAML.dump("invalid: yaml: test")
expect(GitlabCiYamlProcessor.validation_message(content))
.to eq "Invalid configuration format"
end
end
context "when the tags parameter is invalid" do
it "returns an error about invalid tags" do
content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect(GitlabCiYamlProcessor.validation_message(content))
.to eq "jobs:rspec tags should be an array of strings"
end
end
context "when YAML content is empty" do
it "returns an error about missing content" do
expect(GitlabCiYamlProcessor.validation_message(''))
.to eq "Please provide content of .gitlab-ci.yml"
end
end
context "when the YAML is valid" do
it "does not return any errors" do
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
end
end
end
end
end
......@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do
let(:entry) { described_class.new(config) }
describe 'validations' do
before { entry.process! }
before { entry.compose! }
context 'when entry config value is correct' do
let(:config) do
......
......@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do
.value(nil)
.create!
expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
expect(entry)
.to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
......
......@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is valid' do
context 'when all entries defined' do
context 'when some entries defined' do
let(:hash) do
{ before_script: ['ls', 'pwd'],
image: 'ruby:2.2',
......@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do
stages: ['build', 'pages'],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
spinach: { script: 'spinach' } }
spinach: { before_script: [], variables: {}, script: 'spinach' } }
end
describe '#process!' do
before { global.process! }
describe '#compose!' do
before { global.compose! }
it 'creates nodes hash' do
expect(global.descendants).to be_an Array
......@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
context 'when not processed' do
context 'when not composed' do
describe '#before_script' do
it 'returns nil' do
expect(global.before_script).to be nil
......@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
context 'when processed' do
before { global.process! }
context 'when composed' do
before { global.compose! }
describe '#errors' do
it 'has no errors' do
expect(global.errors).to be_empty
end
end
describe '#before_script' do
it 'returns correct script' do
......@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.jobs).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
stage: 'test' },
before_script: ['ls', 'pwd'],
commands: "ls\npwd\nrspec\nls",
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: { VAR: 'value' },
after_script: ['make clean'] },
spinach: { name: :spinach,
before_script: [],
script: %w[spinach],
stage: 'test' }
commands: 'spinach',
image: 'ruby:2.2',
services: ['postgres:9.1', 'mysql:5.5'],
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: {},
after_script: ['make clean'] },
)
end
end
......@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when most of entires not defined' do
let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
before { global.process! }
before { global.compose! }
let(:hash) do
{ cache: { key: 'a' }, rspec: { script: %w[ls] } }
end
describe '#nodes' do
it 'instantizes all nodes' do
expect(global.descendants.count).to eq 8
end
it 'contains undefined nodes' do
it 'contains unspecified nodes' do
expect(global.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
.to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
......@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do
# details.
#
context 'when entires specified but not defined' do
let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
before { global.process! }
before { global.compose! }
let(:hash) do
{ variables: nil, rspec: { script: 'rspec' } }
end
describe '#variables' do
it 'undefined entry returns a default value' do
......@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is not valid' do
before { global.process! }
before { global.compose! }
let(:hash) do
{ before_script: 'ls' }
......@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.specified?).to be true
end
end
describe '#[]' do
before { global.compose! }
let(:hash) do
{ cache: { key: 'a' }, rspec: { script: 'ls' } }
end
context 'when node exists' do
it 'returns correct entry' do
expect(global[:cache])
.to be_an_instance_of Gitlab::Ci::Config::Node::Cache
expect(global[:jobs][:rspec][:script].value).to eq ['ls']
end
end
context 'when node does not exist' do
it 'always return unspecified node' do
expect(global[:some][:unknown][:node])
.not_to be_specified
end
end
end
end
......@@ -3,9 +3,9 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Job do
let(:entry) { described_class.new(config, name: :rspec) }
before { entry.process! }
describe 'validations' do
before { entry.compose! }
context 'when entry config value is correct' do
let(:config) { { script: 'rspec' } }
......@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do
end
end
describe '#value' do
context 'when entry is correct' do
describe '#relevant?' do
it 'is a relevant entry' do
expect(entry).to be_relevant
end
end
describe '#compose!' do
let(:unspecified) { double('unspecified', 'specified?' => false) }
let(:specified) do
double('specified', 'specified?' => true, value: 'specified')
end
let(:deps) { double('deps', '[]' => unspecified) }
context 'when job config overrides global config' do
before { entry.compose!(deps) }
let(:config) do
{ before_script: %w[ls pwd],
script: 'rspec',
after_script: %w[cleanup] }
{ image: 'some_image', cache: { key: 'test' } }
end
it 'overrides global config' do
expect(entry[:image].value).to eq 'some_image'
expect(entry[:cache].value).to eq(key: 'test')
end
end
context 'when job config does not override global config' do
before do
allow(deps).to receive('[]').with(:image).and_return(specified)
entry.compose!(deps)
end
it 'returns correct value' do
expect(entry.value)
.to eq(name: :rspec,
before_script: %w[ls pwd],
script: %w[rspec],
stage: 'test',
after_script: %w[cleanup])
let(:config) { { script: 'ls', cache: { key: 'test' } } }
it 'uses config from global entry' do
expect(entry[:image].value).to eq 'specified'
expect(entry[:cache].value).to eq(key: 'test')
end
end
end
describe '#relevant?' do
it 'is a relevant entry' do
expect(entry).to be_relevant
context 'when composed' do
before { entry.compose! }
describe '#value' do
before { entry.compose! }
context 'when entry is correct' do
let(:config) do
{ before_script: %w[ls pwd],
script: 'rspec',
after_script: %w[cleanup] }
end
it 'returns correct value' do
expect(entry.value)
.to eq(name: :rspec,
before_script: %w[ls pwd],
script: %w[rspec],
commands: "ls\npwd\nrspec",
stage: 'test',
after_script: %w[cleanup])
end
end
end
describe '#commands' do
let(:config) do
{ before_script: %w[ls pwd], script: 'rspec' }
end
it 'returns a string of commands concatenated with new line character' do
expect(entry.commands).to eq "ls\npwd\nrspec"
end
end
end
end
......@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
let(:entry) { described_class.new(config) }
describe 'validations' do
before { entry.process! }
before { entry.compose! }
context 'when entry config value is correct' do
let(:config) { { rspec: { script: 'rspec' } } }
......@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do
end
end
context 'when valid job entries processed' do
before { entry.process! }
context 'when valid job entries composed' do
before { entry.compose! }
let(:config) do
{ rspec: { script: 'rspec' },
......@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.value).to eq(
rspec: { name: :rspec,
script: %w[rspec],
commands: 'rspec',
stage: 'test' },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
stage: 'test' })
end
end
......
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Null do
let(:null) { described_class.new(nil) }
describe '#leaf?' do
it 'is leaf node' do
expect(null).to be_leaf
end
end
describe '#valid?' do
it 'is always valid' do
expect(null).to be_valid
end
end
describe '#errors' do
it 'is does not contain errors' do
expect(null.errors).to be_empty
end
end
describe '#value' do
it 'returns nil' do
expect(null.value).to eq nil
end
end
describe '#relevant?' do
it 'is not relevant' do
expect(null.relevant?).to eq false
end
end
describe '#specified?' do
it 'is not defined' do
expect(null.specified?).to eq false
end
end
end
......@@ -3,9 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(config) }
describe '#process!' do
before { entry.process! }
describe 'validations' do
context 'when entry config value is correct' do
let(:config) { ['ls', 'pwd'] }
......
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
let(:undefined) { described_class.new(entry) }
let(:entry) { spy('Entry') }
let(:entry) { described_class.new }
describe '#leaf?' do
it 'is leaf node' do
expect(entry).to be_leaf
end
end
describe '#valid?' do
it 'delegates method to entry' do
expect(undefined.valid).to eq entry
it 'is always valid' do
expect(entry).to be_valid
end
end
describe '#errors' do
it 'delegates method to entry' do
expect(undefined.errors).to eq entry
it 'is does not contain errors' do
expect(entry.errors).to be_empty
end
end
describe '#value' do
it 'delegates method to entry' do
expect(undefined.value).to eq entry
it 'returns nil' do
expect(entry.value).to eq nil
end
end
describe '#specified?' do
it 'is always false' do
allow(entry).to receive(:specified?).and_return(true)
describe '#relevant?' do
it 'is not relevant' do
expect(entry.relevant?).to eq false
end
end
expect(undefined.specified?).to be false
describe '#specified?' do
it 'is not defined' do
expect(entry.specified?).to eq false
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Unspecified do
let(:unspecified) { described_class.new(entry) }
let(:entry) { spy('Entry') }
describe '#valid?' do
it 'delegates method to entry' do
expect(unspecified.valid?).to eq entry
end
end
describe '#errors' do
it 'delegates method to entry' do
expect(unspecified.errors).to eq entry
end
end
describe '#value' do
it 'delegates method to entry' do
expect(unspecified.value).to eq entry
end
end
describe '#specified?' do
it 'is always false' do
allow(entry).to receive(:specified?).and_return(true)
expect(unspecified.specified?).to be false
end
end
end
......@@ -179,8 +179,8 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
it 'raises UnmergeableFile when the file is over 100 KB' do
expect { parse_text('a' * 102401) }.
it 'raises UnmergeableFile when the file is over 200 KB' do
expect { parse_text('a' * 204801) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
......
......@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
'title' => 'Issue',
'description' => 'Lorem ipsum',
'state' => 'opened',
'confidential' => true,
'author' => {
'id' => 283999,
'name' => 'John Doe'
......@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
title: 'Issue',
description: "*Created by: John Doe*\n\nLorem ipsum",
state: 'opened',
confidential: true,
author_id: project.creator_id
}
......
......@@ -31,6 +31,43 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
describe ".resolve!" do
let(:current_user) { create(:user) }
let!(:commit_note) { create(:diff_note_on_commit) }
let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
let!(:unresolved_note) { create(:diff_note_on_merge_request) }
before do
described_class.resolve!(current_user)
commit_note.reload
resolved_note.reload
unresolved_note.reload
end
it 'resolves only the resolvable, not yet resolved notes' do
expect(commit_note.resolved_at).to be_nil
expect(resolved_note.resolved_by).not_to eq(current_user)
expect(unresolved_note.resolved_at).not_to be_nil
expect(unresolved_note.resolved_by).to eq(current_user)
end
end
describe ".unresolve!" do
let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
before do
described_class.unresolve!
resolved_note.reload
end
it 'unresolves the resolved notes' do
expect(resolved_note.resolved_by).to be_nil
expect(resolved_note.resolved_at).to be_nil
end
end
describe "#position=" do
context "when provided a string" do
it "sets the position" do
......
......@@ -238,27 +238,19 @@ describe Discussion, model: true do
context "when resolvable" do
let(:user) { create(:user) }
let(:second_note) { create(:diff_note_on_commit) } # unresolvable
before do
allow(subject).to receive(:resolvable?).and_return(true)
allow(first_note).to receive(:resolvable?).and_return(true)
allow(second_note).to receive(:resolvable?).and_return(false)
allow(third_note).to receive(:resolvable?).and_return(true)
end
context "when all resolvable notes are resolved" do
before do
first_note.resolve!(user)
third_note.resolve!(user)
end
it "calls resolve! on every resolvable note" do
expect(first_note).to receive(:resolve!).with(current_user)
expect(second_note).not_to receive(:resolve!)
expect(third_note).to receive(:resolve!).with(current_user)
subject.resolve!(current_user)
first_note.reload
third_note.reload
end
it "doesn't change resolved_at on the resolved notes" do
......@@ -309,46 +301,44 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
it "calls resolve! on every resolvable note" do
expect(first_note).to receive(:resolve!).with(current_user)
expect(second_note).not_to receive(:resolve!)
expect(third_note).to receive(:resolve!).with(current_user)
subject.resolve!(current_user)
end
it "doesn't change resolved_at on the resolved note" do
expect(first_note.resolved_at).not_to be_nil
expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
expect { subject.resolve!(current_user) }.
not_to change { first_note.reload.resolved_at }
end
it "doesn't change resolved_by on the resolved note" do
expect(first_note.resolved_by).to eq(user)
expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
expect { subject.resolve!(current_user) }.
not_to change { first_note.reload && first_note.resolved_by }
end
it "doesn't change the resolved state on the resolved note" do
expect(first_note.resolved?).to be true
expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
expect { subject.resolve!(current_user) }.
not_to change { first_note.reload && first_note.resolved? }
end
it "sets resolved_at on the unresolved note" do
subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved_at).not_to be_nil
end
it "sets resolved_by on the unresolved note" do
subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved_by).to eq(current_user)
end
it "marks the unresolved note as resolved" do
subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved?).to be true
end
......@@ -373,16 +363,10 @@ describe Discussion, model: true do
end
context "when no resolvable notes are resolved" do
it "calls resolve! on every resolvable note" do
expect(first_note).to receive(:resolve!).with(current_user)
expect(second_note).not_to receive(:resolve!)
expect(third_note).to receive(:resolve!).with(current_user)
subject.resolve!(current_user)
end
it "sets resolved_at on the unresolved notes" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved_at).not_to be_nil
expect(third_note.resolved_at).not_to be_nil
......@@ -390,6 +374,8 @@ describe Discussion, model: true do
it "sets resolved_by on the unresolved notes" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved_by).to eq(current_user)
expect(third_note.resolved_by).to eq(current_user)
......@@ -397,6 +383,8 @@ describe Discussion, model: true do
it "marks the unresolved notes as resolved" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved?).to be true
expect(third_note.resolved?).to be true
......@@ -404,18 +392,24 @@ describe Discussion, model: true do
it "sets resolved_at" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved_at).not_to be_nil
end
it "sets resolved_by" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved_by).to eq(current_user)
end
it "marks as resolved" do
subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved?).to be true
end
......@@ -451,16 +445,10 @@ describe Discussion, model: true do
third_note.resolve!(user)
end
it "calls unresolve! on every resolvable note" do
expect(first_note).to receive(:unresolve!)
expect(second_note).not_to receive(:unresolve!)
expect(third_note).to receive(:unresolve!)
subject.unresolve!
end
it "unsets resolved_at on the resolved notes" do
subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved_at).to be_nil
expect(third_note.resolved_at).to be_nil
......@@ -468,6 +456,8 @@ describe Discussion, model: true do
it "unsets resolved_by on the resolved notes" do
subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved_by).to be_nil
expect(third_note.resolved_by).to be_nil
......@@ -475,6 +465,8 @@ describe Discussion, model: true do
it "unmarks the resolved notes as resolved" do
subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved?).to be false
expect(third_note.resolved?).to be false
......@@ -482,12 +474,16 @@ describe Discussion, model: true do
it "unsets resolved_at" do
subject.unresolve!
first_note.reload
third_note.reload
expect(subject.resolved_at).to be_nil
end
it "unsets resolved_by" do
subject.unresolve!
first_note.reload
third_note.reload
expect(subject.resolved_by).to be_nil
end
......@@ -504,40 +500,22 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
it "calls unresolve! on every resolvable note" do
expect(first_note).to receive(:unresolve!)
expect(second_note).not_to receive(:unresolve!)
expect(third_note).to receive(:unresolve!)
subject.unresolve!
end
it "unsets resolved_at on the resolved note" do
subject.unresolve!
expect(first_note.resolved_at).to be_nil
expect(subject.first_note.resolved_at).to be_nil
end
it "unsets resolved_by on the resolved note" do
subject.unresolve!
expect(first_note.resolved_by).to be_nil
expect(subject.first_note.resolved_by).to be_nil
end
it "unmarks the resolved note as resolved" do
subject.unresolve!
expect(first_note.resolved?).to be false
end
end
context "when no resolvable notes are resolved" do
it "calls unresolve! on every resolvable note" do
expect(first_note).to receive(:unresolve!)
expect(second_note).not_to receive(:unresolve!)
expect(third_note).to receive(:unresolve!)
subject.unresolve!
expect(subject.first_note.resolved?).to be false
end
end
end
......
......@@ -441,7 +441,7 @@ describe Repository, models: true do
end
end
describe '#commit_with_hooks' do
describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
......@@ -454,31 +454,64 @@ describe Repository, models: true do
it 'runs without errors' do
expect do
repository.commit_with_hooks(user, 'feature') { new_rev }
repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
expect(repository).to receive(:update_autocrlf_option)
repository.commit_with_hooks(user, 'feature') { new_rev }
repository.update_branch_with_hooks(user, 'feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').target.id).to eq(old_rev)
repository.commit_with_hooks(user, 'feature') { new_rev }
repository.update_branch_with_hooks(user, 'feature') { new_rev }
expect(repository.find_branch('feature').target.id).to eq(new_rev)
end
end
end
context 'when the update adds more than one commit' do
it 'runs without errors' do
old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
# old_rev is an ancestor of new_rev
expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
# old_rev is not a direct ancestor (parent) of new_rev
expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
branch = 'feature-ff-target'
repository.add_branch(user, branch, old_rev)
expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
end
end
context 'when the update would remove commits from the target branch' do
it 'raises an exception' do
branch = 'master'
old_rev = repository.find_branch(branch).target.sha
# The 'master' branch is NOT an ancestor of new_rev.
expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
repository.update_branch_with_hooks(user, branch) { new_rev }
end.to raise_error(Repository::CommitError)
end
end
context 'when pre hooks failed' do
it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.commit_with_hooks(user, 'feature') { new_rev }
repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.to raise_error(GitHooksService::PreReceiveError)
end
end
......@@ -497,7 +530,7 @@ describe Repository, models: true do
expect(repository).to receive(:expire_has_visible_content_cache)
expect(repository).to receive(:expire_branch_count_cache)
repository.commit_with_hooks(user, 'new-feature') { new_rev }
repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end
end
......
......@@ -6,6 +6,12 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group) }
let(:group2) do
group = create(:group, name: 'group2_name')
group.add_owner(user2)
group
end
let(:project) do
create(:project, creator_id: user.id, namespace: user.namespace)
......@@ -22,6 +28,7 @@ describe API::API, api: true do
context 'when authenticated' do
it 'forks if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
......@@ -32,6 +39,7 @@ describe API::API, api: true do
it 'forks if user is admin' do
post api("/projects/fork/#{project.id}", admin)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
......@@ -42,12 +50,14 @@ describe API::API, api: true do
it 'fails on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'fails if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
......@@ -55,14 +65,70 @@ describe API::API, api: true do
it 'fails if project to fork from does not exist' do
post api('/projects/fork/424242', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'forks with explicit own user namespace id' do
post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id
expect(response).to have_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks with explicit own user name as namespace' do
post api("/projects/fork/#{project.id}", user2), namespace: user2.username
expect(response).to have_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks to another user when admin' do
post api("/projects/fork/#{project.id}", admin), namespace: user2.username
expect(response).to have_http_status(201)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'fails if trying to fork to another user when not admin' do
post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
expect(response).to have_http_status(409)
end
it 'fails if trying to fork to non-existent namespace' do
post api("/projects/fork/#{project.id}", user2), namespace: 42424242
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Target Namespace Not Found')
end
it 'forks to owned group' do
post api("/projects/fork/#{project.id}", user2), namespace: group2.name
expect(response).to have_http_status(201)
expect(json_response['namespace']['name']).to eq(group2.name)
end
it 'fails to fork to not owned group' do
post api("/projects/fork/#{project.id}", user2), namespace: group.name
expect(response).to have_http_status(409)
end
it 'forks to not owned group when admin' do
post api("/projects/fork/#{project.id}", admin), namespace: group.name
expect(response).to have_http_status(201)
expect(json_response['namespace']['name']).to eq(group.name)
end
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/projects/fork/#{project.id}")
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
......
require 'spec_helper'
describe API::Lint, api: true do
include ApiHelpers
describe 'POST /ci/lint' do
context 'with valid .gitlab-ci.yaml content' do
let(:yaml_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
it 'passes validation' do
post api('/ci/lint'), { content: yaml_content }
expect(response).to have_http_status(200)
expect(json_response).to be_an Hash
expect(json_response['status']).to eq('valid')
expect(json_response['errors']).to eq([])
end
end
context 'with an invalid .gitlab_ci.yml' do
it 'responds with errors about invalid syntax' do
post api('/ci/lint'), { content: 'invalid content' }
expect(response).to have_http_status(200)
expect(json_response['status']).to eq('invalid')
expect(json_response['errors']).to eq(['Invalid configuration format'])
end
it "responds with errors about invalid configuration" do
post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' }
expect(response).to have_http_status(200)
expect(json_response['status']).to eq('invalid')
expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
end
end
context 'without the content parameter' do
it 'responds with validation error about missing content' do
post api('/ci/lint')
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('content is missing')
end
end
end
end
......@@ -15,7 +15,7 @@ RSpec.configure do |config|
DatabaseCleaner.start
end
config.after(:each) do
config.append_after(:each) do
DatabaseCleaner.clean
end
end
require 'spec_helper'
describe PruneOldEventsWorker do
describe '#perform' do
let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) }
let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
it 'prunes events older than 12 months' do
expect { subject.perform }.to change { Event.count }.by(-1)
expect(Event.find_by(id: expired_event.id)).to be_nil
end
it 'leaves fresh events' do
subject.perform
expect(not_expired_event.reload).to be_present
end
it 'leaves events from exactly 12 months ago' do
subject.perform
expect(exactly_12_months_event).to be_present
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