Commit 80088bfa authored by Ruben Davila's avatar Ruben Davila

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

Conflicts:
	app/views/shared/issuable/_filter.html.haml
	lib/api/api.rb
parents 88e44726 5533fd17
...@@ -84,7 +84,7 @@ update-knapsack: ...@@ -84,7 +84,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- knapsack rspec - knapsack rspec "--color --format documentation"
artifacts: artifacts:
expire_in: 31d expire_in: 31d
paths: paths:
......
...@@ -6,7 +6,10 @@ v 8.11.1 ...@@ -6,7 +6,10 @@ v 8.11.1
- Fix file links on project page when default view is Files !5933 - Fix file links on project page when default view is Files !5933
- Fixed enter key in search input not working !5888 - Fixed enter key in search input not working !5888
v 8.12.0 (unreleased) 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 - 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) - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Filter tags by name !6121 - Filter tags by name !6121
- Make push events have equal vertical spacing. - Make push events have equal vertical spacing.
...@@ -16,22 +19,28 @@ v 8.12.0 (unreleased) ...@@ -16,22 +19,28 @@ v 8.12.0 (unreleased)
- Change logo animation to CSS (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104 - Instructions for enabling Git packfile bitmaps !6104
- Fix pagination on user snippets page - 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 - Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps) - Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - 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) - 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) - 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) - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Center build stage columns in pipeline overview (ClemMakesApps) - Center build stage columns in pipeline overview (ClemMakesApps)
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps) - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
- Remove suggested colors hover underline (ClemMakesApps) - Remove suggested colors hover underline (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps) - Shorten task status phrase (ClemMakesApps)
- Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps) - Add hover color to emoji icon (ClemMakesApps)
- Add textarea autoresize after comment (ClemMakesApps)
- Fix branches page dropdown sort alignment (ClemMakesApps) - Fix branches page dropdown sort alignment (ClemMakesApps)
- Add white background for no readme container (ClemMakesApps) - Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling) - API: Expose issue confidentiality flag. (Robert Schilling)
- Fix markdown anchor icon interaction (ClemMakesApps) - Fix markdown anchor icon interaction (ClemMakesApps)
- Test migration paths from 8.5 until current release !4874 - 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) - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import - Remove Gitorious import
...@@ -40,6 +49,7 @@ v 8.12.0 (unreleased) ...@@ -40,6 +49,7 @@ v 8.12.0 (unreleased)
- Add Sentry logging to API calls - Add Sentry logging to API calls
- Add BroadcastMessage API - Add BroadcastMessage API
- Use 'git update-ref' for safer web commits !6130 - 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) - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Remove unused mixins (ClemMakesApps) - Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists - Add search to all issue board lists
...@@ -59,6 +69,7 @@ v 8.12.0 (unreleased) ...@@ -59,6 +69,7 @@ v 8.12.0 (unreleased)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level - Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Remove inconsistent font weight for sidebar's labels (ClemMakesApps) - Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
- Align add button on repository view (ClemMakesApps) - Align add button on repository view (ClemMakesApps)
- Fix contributions calendar month label truncation (ClemMakesApps)
- Added tests for diff notes - Added tests for diff notes
- Add a button to download latest successful artifacts for branches and tags !5142 - Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps) - Remove redundant pipeline tooltips (ClemMakesApps)
...@@ -67,6 +78,7 @@ v 8.12.0 (unreleased) ...@@ -67,6 +78,7 @@ v 8.12.0 (unreleased)
- Fix badge count alignment (ClemMakesApps) - Fix badge count alignment (ClemMakesApps)
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell) - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps) - Fix repo title alignment (ClemMakesApps)
- Change update interval of contacted_at
- Fix branch title trailing space on hover (ClemMakesApps) - Fix branch title trailing space on hover (ClemMakesApps)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison) - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison) - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
...@@ -96,13 +108,21 @@ v 8.12.0 (unreleased) ...@@ -96,13 +108,21 @@ v 8.12.0 (unreleased)
- Refactor the triggers page and documentation !6217 - Refactor the triggers page and documentation !6217
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska) - 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 - 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) v 8.11.6 (unreleased)
- Optimize branch lookups and force a repository reload for Repository#find_branch
- Fix member expiration date picker after update 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 - Fix suggested colors options for new labels in the admin area. !6138
- Optimize discussion notes resolving and unresolving
- Fix GitLab import button - 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 - Remove gitorious from import_sources
- Fix confidential issues being exposed as public using gitlab.com export
v 8.11.4 v 8.11.4
- Fix resolving conflicts on forks. !6082 - Fix resolving conflicts on forks. !6082
...@@ -116,13 +136,10 @@ v 8.11.4 ...@@ -116,13 +136,10 @@ v 8.11.4
- Creating an issue through our API now emails label subscribers !5720 - Creating an issue through our API now emails label subscribers !5720
- Block concurrent updates for Pipeline - Block concurrent updates for Pipeline
- Don't create groups for unallowed users when importing projects - 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 issue boards leak private label names and descriptions
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner) - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Remove gitorious. !5866 - Remove gitorious. !5866
- Allow compare merge request versions
v 8.11.3 v 8.11.3
- Allow system info page to handle case where info is unavailable - Allow system info page to handle case where info is unavailable
...@@ -135,6 +152,7 @@ v 8.11.3 ...@@ -135,6 +152,7 @@ v 8.11.3
- Fix external issue tracker "Issues" link leading to 404s - 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 - 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) - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Issues filters reset button
v 8.11.2 v 8.11.2
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978 - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
......
...@@ -608,7 +608,7 @@ GEM ...@@ -608,7 +608,7 @@ GEM
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (2.0.5) rouge (2.0.6)
rqrcode (0.7.0) rqrcode (0.7.0)
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
......
...@@ -177,9 +177,7 @@ ...@@ -177,9 +177,7 @@
$body.tooltip({ $body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]', selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) { placement: function(_, el) {
var $el; return $(el).data('placement') || 'bottom';
$el = $(el);
return $el.data('placement') || 'bottom';
} }
}); });
$('.trigger-submit').on('change', function() { $('.trigger-submit').on('change', function() {
...@@ -292,42 +290,9 @@ ...@@ -292,42 +290,9 @@
gl.awardsHandler = new AwardsHandler(); gl.awardsHandler = new AwardsHandler();
checkInitialSidebarSize(); checkInitialSidebarSize();
new Aside(); new Aside();
if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
$.cookie('pin_nav', 'false', { // bind sidebar events
path: gon.relative_url_root || '/', new gl.Sidebar();
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');
});
// Custom time ago // Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago')); gl.utils.shortTimeAgo($('.js-short-timeago'));
......
...@@ -255,12 +255,12 @@ ...@@ -255,12 +255,12 @@
}; };
AwardsHandler.prototype.animateEmoji = function($emoji) { AwardsHandler.prototype.animateEmoji = function($emoji) {
var className; var className = 'pulse animated once short';
className = 'pulse animated';
$emoji.addClass(className); $emoji.addClass(className);
return setTimeout((function() {
return $emoji.removeClass(className); $emoji.on('webkitAnimationEnd animationEnd', function() {
}), 321); $(this).removeClass(className);
});
}; };
AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
Issuable.initTemplates(); Issuable.initTemplates();
Issuable.initSearch(); Issuable.initSearch();
Issuable.initChecks(); Issuable.initChecks();
Issuable.initResetFilters();
return Issuable.initLabelFilterRemove(); return Issuable.initLabelFilterRemove();
}, },
initTemplates: function() { initTemplates: function() {
...@@ -55,6 +56,17 @@ ...@@ -55,6 +56,17 @@
return Turbolinks.visit(issuesUrl); return Turbolinks.visit(issuesUrl);
}; };
})(this), })(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() { initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions'); this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() { $('.check_all_issues').off('click').on('click', function() {
...@@ -64,19 +76,22 @@ ...@@ -64,19 +76,22 @@
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
}, },
checkChanged: function() { checkChanged: function() {
var checked_issues, ids; const $checkedIssues = $('.selected_issue:checked');
checked_issues = $('.selected_issue:checked'); const $updateIssuesIds = $('#update_issues_ids');
if (checked_issues.length > 0) { const $issuesOtherFilters = $('.issues-other-filters');
ids = $.map(checked_issues, function(value) { const $issuesBulkUpdate = $('.issues_bulk_update');
if ($checkedIssues.length > 0) {
let ids = $.map($checkedIssues, function(value) {
return $(value).data('id'); return $(value).data('id');
}); });
$('#update_issues_ids').val(ids); $updateIssuesIds.val(ids);
$('.issues-other-filters').hide(); $issuesOtherFilters.hide();
$('.issues_bulk_update').show(); $issuesBulkUpdate.show();
} else { } else {
$('#update_issues_ids').val([]); $updateIssuesIds.val([]);
$('.issues_bulk_update').hide(); $issuesBulkUpdate.hide();
$('.issues-other-filters').show(); $issuesOtherFilters.show();
this.issuableBulkActions.willUpdateLabels = false; this.issuableBulkActions.willUpdateLabels = false;
} }
return true; return true;
......
...@@ -331,7 +331,12 @@ ...@@ -331,7 +331,12 @@
form.find(".js-md-write-button").click(); form.find(".js-md-write-button").click();
form.find(".js-note-text").val("").trigger("input"); form.find(".js-note-text").val("").trigger("input");
form.find(".js-note-text").data("autosave").reset(); 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() { 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 @@ ...@@ -52,8 +52,22 @@
this.initTooltips(); 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) { 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() { Calendar.prototype.renderDays = function() {
......
...@@ -8,65 +8,44 @@ ...@@ -8,65 +8,44 @@
// Copyright (c) 2016 Daniel Eden // Copyright (c) 2016 Daniel Eden
.animated { .animated {
-webkit-animation-duration: 1s; @include webkit-prefix(animation-duration, 1s);
animation-duration: 1s; @include webkit-prefix(animation-fill-mode, both);
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge { &.infinite {
-webkit-animation-duration: 2s; @include webkit-prefix(animation-iteration-count, infinite);
animation-duration: 2s; }
}
.animated.flipOutX, &.once {
.animated.flipOutY, @include webkit-prefix(animation-iteration-count, 1);
.animated.bounceIn, }
.animated.bounceOut {
-webkit-animation-duration: .75s;
animation-duration: .75s;
}
@-webkit-keyframes pulse { &.hinge {
from { @include webkit-prefix(animation-duration, 2s);
-webkit-transform: scale3d(1, 1, 1);
transform: scale3d(1, 1, 1);
} }
50% { &.flipOutX,
-webkit-transform: scale3d(1.05, 1.05, 1.05); &.flipOutY,
transform: scale3d(1.05, 1.05, 1.05); &.bounceIn,
&.bounceOut {
@include webkit-prefix(animation-duration, .75s);
} }
to { &.short {
-webkit-transform: scale3d(1, 1, 1); @include webkit-prefix(animation-duration, 321ms);
transform: scale3d(1, 1, 1); @include webkit-prefix(animation-fill-mode, none);
} }
} }
@keyframes pulse { @include keyframes(pulse) {
from { from, to {
-webkit-transform: scale3d(1, 1, 1); @include webkit-prefix(transform, scale3d(1, 1, 1));
transform: scale3d(1, 1, 1);
} }
50% { 50% {
-webkit-transform: scale3d(1.05, 1.05, 1.05); @include webkit-prefix(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);
} }
} }
.pulse { .pulse {
-webkit-animation-name: pulse; @include webkit-prefix(animation-name, pulse);
animation-name: pulse;
} }
...@@ -249,6 +249,10 @@ ...@@ -249,6 +249,10 @@
> .controls { > .controls {
float: right; float: right;
} }
.new-branch {
margin-top: 3px;
}
} }
.content-block-small { .content-block-small {
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
&.image_file { &.image_file {
background: #eee; background: #eee;
text-align: center; text-align: center;
img { img {
padding: 20px; padding: 20px;
max-width: 80%; max-width: 80%;
...@@ -94,7 +94,6 @@ ...@@ -94,7 +94,6 @@
&.blame { &.blame {
table { table {
border: none; border: none;
box-shadow: none;
margin: 0; margin: 0;
} }
tr { tr {
...@@ -108,19 +107,10 @@ ...@@ -108,19 +107,10 @@
border-right: none; border-right: none;
} }
} }
img.avatar {
border: 0 none;
float: none;
margin: 0;
padding: 0;
}
td.blame-commit { td.blame-commit {
padding: 0 10px;
min-width: 400px;
background: $gray-light; background: $gray-light;
min-width: 350px;
.commit-author-link {
color: #888;
}
} }
td.line-numbers { td.line-numbers {
float: none; float: none;
...@@ -133,12 +123,6 @@ ...@@ -133,12 +123,6 @@
} }
td.lines { td.lines {
padding: 0; padding: 0;
code {
font-family: $monospace_font;
}
pre {
margin: 0;
}
} }
} }
......
.filter-item { .filter-item {
margin-right: 6px; margin-right: 6px;
vertical-align: top; vertical-align: top;
&.reset-filters {
padding: 7px;
}
} }
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -77,10 +77,6 @@ header { ...@@ -77,10 +77,6 @@ header {
} }
} }
&.header-collapsed {
padding: 0 16px;
}
.side-nav-toggle { .side-nav-toggle {
position: absolute; position: absolute;
left: -10px; left: -10px;
......
...@@ -85,3 +85,13 @@ ...@@ -85,3 +85,13 @@
#{'-webkit-' + $property}: $value; #{'-webkit-' + $property}: $value;
#{$property}: $value; #{$property}: $value;
} }
@mixin keyframes($animation-name) {
@-webkit-keyframes #{$animation-name} {
@content;
}
@keyframes #{$animation-name} {
@content;
}
}
.page-with-sidebar { .page-with-sidebar {
padding-top: $header-height; padding: $header-height 0 25px;
padding-bottom: 25px;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned { &.page-sidebar-pinned {
...@@ -15,6 +14,7 @@ ...@@ -15,6 +14,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 0;
overflow: hidden; overflow: hidden;
transition: width $sidebar-transition-duration; transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent); @include box-shadow(2px 0 16px 0 $black-transparent);
...@@ -128,10 +128,8 @@ ...@@ -128,10 +128,8 @@
.fa { .fa {
transition: transform .15s; transition: transform .15s;
}
&.is-active { .page-sidebar-pinned & {
.fa {
transform: rotate(90deg); transform: rotate(90deg);
} }
} }
...@@ -152,14 +150,6 @@ ...@@ -152,14 +150,6 @@
} }
} }
.page-sidebar-collapsed {
padding-left: 0;
.sidebar-wrapper {
width: 0;
}
}
.page-sidebar-expanded { .page-sidebar-expanded {
.sidebar-wrapper { .sidebar-wrapper {
width: $sidebar_width; width: $sidebar_width;
...@@ -175,7 +165,7 @@ ...@@ -175,7 +165,7 @@
} }
} }
header.header-pinned-nav { header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) { @media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding); padding-left: ($sidebar_width + $gl-padding);
......
...@@ -93,11 +93,8 @@ ...@@ -93,11 +93,8 @@
} }
.award-control { .award-control {
margin-right: 5px; margin: 3px 5px 3px 0;
margin-bottom: 5px; padding: 6px 5px;
padding-left: 5px;
padding-right: 5px;
line-height: 20px;
outline: 0; outline: 0;
&:hover, &:hover,
......
...@@ -48,12 +48,6 @@ ...@@ -48,12 +48,6 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
.page-sidebar-collapsed {
.scroll-controls {
left: 70px;
}
}
} }
.build-header { .build-header {
......
...@@ -375,7 +375,7 @@ ...@@ -375,7 +375,7 @@
} }
} }
.mr-version-switch { .mr-version-controls {
background: $background-color; background: $background-color;
padding: $gl-btn-padding; padding: $gl-btn-padding;
color: $gl-placeholder-color; color: $gl-placeholder-color;
......
...@@ -490,6 +490,6 @@ ...@@ -490,6 +490,6 @@
.ci-status-icon-created { .ci-status-icon-created {
svg { svg {
fill: $table-text-gray; fill: $gray-darkest;
} }
} }
...@@ -7,19 +7,14 @@ module Ci ...@@ -7,19 +7,14 @@ module Ci
def create def create
@content = params[:content] @content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content)
@status = @error.blank?
if @content.blank? if @error.blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
@config_processor = Ci::GitlabCiYamlProcessor.new(@content) @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages @stages = @config_processor.stages
@builds = @config_processor.builds @builds = @config_processor.builds
@status = true
end end
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message
@status = false
rescue rescue
@error = 'Undefined error' @error = 'Undefined error'
@status = false @status = false
......
...@@ -91,16 +91,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -91,16 +91,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.merge_request_diff @merge_request.merge_request_diff
end 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| respond_to do |format|
format.html { define_discussion_vars } format.html { define_discussion_vars }
format.json do format.json do
unless @merge_request_diff.latest? if @start_sha
# Disable comments if browsing older version of the diff compared_diff_version
@diff_notes_disabled = true else
original_diff_version
end end
@diffs = @merge_request_diff.diffs(diff_options)
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end end
end end
...@@ -588,4 +599,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -588,4 +599,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end 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 end
...@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
all_pipelines = project.pipelines @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) @pipelines_count = PipelinesFinder.new(project).execute.count
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
end end
def new def new
......
class PipelinesFinder class PipelinesFinder
attr_reader :project attr_reader :project, :pipelines
def initialize(project) def initialize(project)
@project = project @project = project
@pipelines = project.pipelines
end end
def execute(pipelines, scope) def execute(scope: nil)
case scope scoped_pipelines =
when 'running' case scope
pipelines.running_or_pending when 'running'
when 'branches' pipelines.running_or_pending
from_ids(pipelines, ids_for_ref(pipelines, branches)) when 'branches'
when 'tags' from_ids(ids_for_ref(branches))
from_ids(pipelines, ids_for_ref(pipelines, tags)) when 'tags'
else from_ids(ids_for_ref(tags))
pipelines else
end pipelines
end
scoped_pipelines.order(id: :desc)
end end
private private
def ids_for_ref(pipelines, refs) def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)') pipelines.where(ref: refs).group(:ref).select('max(id)')
end end
def from_ids(pipelines, ids) def from_ids(ids)
pipelines.unscoped.where(id: ids) pipelines.unscoped.where(id: ids)
end end
......
...@@ -14,7 +14,8 @@ module AvatarsHelper ...@@ -14,7 +14,8 @@ module AvatarsHelper
avatar_icon(options[:user] || options[:user_email], avatar_size), avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}", class: "avatar has-tooltip hidden-xs s#{avatar_size}",
alt: "#{user_name}'s avatar", alt: "#{user_name}'s avatar",
title: user_name title: user_name,
data: { container: 'body' }
) )
if options[:user] if options[:user]
......
...@@ -2,4 +2,8 @@ module GitHelper ...@@ -2,4 +2,8 @@ module GitHelper
def strip_gpg_signature(text) def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
end end
def short_sha(text)
Commit.truncate_sha(text)
end
end end
...@@ -137,4 +137,14 @@ module MergeRequestsHelper ...@@ -137,4 +137,14 @@ module MergeRequestsHelper
def merge_request_button_visibility(merge_request, closed) 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? return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end 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 end
module NavHelper 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 def page_sidebar_class
if pinned_nav? if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned" "page-sidebar-expanded page-sidebar-pinned"
else
"page-sidebar-collapsed"
end end
end end
...@@ -26,7 +12,6 @@ module NavHelper ...@@ -26,7 +12,6 @@ module NavHelper
current_path?('merge_requests#builds') || current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') || current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') || current_path?('merge_requests#pipelines') ||
current_path?('issues#show') current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed" "page-gutter right-sidebar-collapsed"
...@@ -43,9 +28,7 @@ module NavHelper ...@@ -43,9 +28,7 @@ module NavHelper
class_name << " with-horizontal-nav" if defined?(nav) && nav class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav? if pinned_nav?
class_name << " header-expanded header-pinned-nav" class_name << " header-sidebar-expanded header-sidebar-pinned"
else
class_name << " header-collapsed"
end end
class_name class_name
......
...@@ -129,6 +129,19 @@ module ProjectsHelper ...@@ -129,6 +129,19 @@ module ProjectsHelper
current_user.recent_push(project_ids) current_user.recent_push(project_ids)
end 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 private
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
...@@ -439,15 +452,4 @@ module ProjectsHelper ...@@ -439,15 +452,4 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED 'Everyone with access' => ProjectFeature::ENABLED
} }
end 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 end
...@@ -7,8 +7,10 @@ module SearchHelper ...@@ -7,8 +7,10 @@ module SearchHelper
projects_autocomplete(term) projects_autocomplete(term)
].flatten ].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i")
generic_results = project_autocomplete + default_autocomplete + help_autocomplete 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, resources_results,
......
...@@ -2,7 +2,7 @@ module Ci ...@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Ci::Model
LAST_CONTACT_TIME = 5.minutes.ago LAST_CONTACT_TIME = 2.hours.ago
AVAILABLE_SCOPES = %w[specific shared active paused online] AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked] FORM_EDITABLE = %i[description tag_list active run_untagged locked]
......
...@@ -13,6 +13,11 @@ class DiffNote < Note ...@@ -13,6 +13,11 @@ class DiffNote < Note
validate :positions_complete validate :positions_complete
validate :verify_supported 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 after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id before_validation :set_line_code, :set_original_discussion_id
...@@ -25,6 +30,16 @@ class DiffNote < Note ...@@ -25,6 +30,16 @@ class DiffNote < Note
def build_discussion_id(noteable_type, noteable_id, position) def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-") [super(noteable_type, noteable_id), *position.key].join("-")
end 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 end
def new_diff_note? def new_diff_note?
...@@ -73,6 +88,7 @@ class DiffNote < Note ...@@ -73,6 +88,7 @@ class DiffNote < Note
self.position.diff_refs == diff_refs self.position.diff_refs == diff_refs
end end
# If you update this method remember to also update the scope `resolvable`
def resolvable? def resolvable?
!system? && for_merge_request? !system? && for_merge_request?
end end
...@@ -83,6 +99,7 @@ class DiffNote < Note ...@@ -83,6 +99,7 @@ class DiffNote < Note
self.resolved_at.present? self.resolved_at.present?
end end
# If you update this method remember to also update `.resolve!`
def resolve!(current_user) def resolve!(current_user)
return unless resolvable? return unless resolvable?
return if resolved? return if resolved?
...@@ -92,6 +109,7 @@ class DiffNote < Note ...@@ -92,6 +109,7 @@ class DiffNote < Note
save! save!
end end
# If you update this method remember to also update `.unresolve!`
def unresolve! def unresolve!
return unless resolvable? return unless resolvable?
return unless resolved? return unless resolved?
......
class Discussion class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16 NUMBER_OF_TRUNCATED_DIFF_LINES = 16
attr_reader :first_note, :last_note, :notes attr_reader :notes
delegate :created_at, delegate :created_at,
:project, :project,
...@@ -36,8 +36,6 @@ class Discussion ...@@ -36,8 +36,6 @@ class Discussion
end end
def initialize(notes) def initialize(notes)
@first_note = notes.first
@last_note = notes.last
@notes = notes @notes = notes
end end
...@@ -70,17 +68,25 @@ class Discussion ...@@ -70,17 +68,25 @@ class Discussion
end end
def resolvable? def resolvable?
return @resolvable if defined?(@resolvable) return @resolvable if @resolvable.present?
@resolvable = diff_discussion? && notes.any?(&:resolvable?) @resolvable = diff_discussion? && notes.any?(&:resolvable?)
end end
def resolved? def resolved?
return @resolved if defined?(@resolved) return @resolved if @resolved.present?
@resolved = resolvable? && notes.none?(&:to_be_resolved?) @resolved = resolvable? && notes.none?(&:to_be_resolved?)
end end
def first_note
@first_note ||= @notes.first
end
def last_note
@last_note ||= @notes.last
end
def resolved_notes def resolved_notes
notes.select(&:resolved?) notes.select(&:resolved?)
end end
...@@ -100,17 +106,13 @@ class Discussion ...@@ -100,17 +106,13 @@ class Discussion
def resolve!(current_user) def resolve!(current_user)
return unless resolvable? return unless resolvable?
notes.each do |note| update { |notes| notes.resolve!(current_user) }
note.resolve!(current_user) if note.resolvable?
end
end end
def unresolve! def unresolve!
return unless resolvable? return unless resolvable?
notes.each do |note| update { |notes| notes.unresolve! }
note.unresolve! if note.resolvable?
end
end end
def for_target?(target) def for_target?(target)
...@@ -118,7 +120,7 @@ class Discussion ...@@ -118,7 +120,7 @@ class Discussion
end end
def active? def active?
return @active if defined?(@active) return @active if @active.present?
@active = first_note.active? @active = first_note.active?
end end
...@@ -174,4 +176,17 @@ class Discussion ...@@ -174,4 +176,17 @@ class Discussion
prev_lines prev_lines
end 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 end
...@@ -152,6 +152,10 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -152,6 +152,10 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff self == merge_request.merge_request_diff
end end
def compare_with(sha)
CompareService.new.execute(project, head_commit_sha, project, sha)
end
private private
def dump_commits(commits) def dump_commits(commits)
......
...@@ -847,7 +847,7 @@ class Repository ...@@ -847,7 +847,7 @@ class Repository
end end
def commit_dir(user, path, message, branch) 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) committer = user_to_committer(user)
options = {} options = {}
options[:committer] = committer options[:committer] = committer
...@@ -864,7 +864,7 @@ class Repository ...@@ -864,7 +864,7 @@ class Repository
end end
def commit_file(user, path, content, message, branch, update) 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) committer = user_to_committer(user)
options = {} options = {}
options[:committer] = committer options[:committer] = committer
...@@ -886,7 +886,7 @@ class Repository ...@@ -886,7 +886,7 @@ class Repository
end end
def update_file(user, path, content, branch:, previous_path:, message:) 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) committer = user_to_committer(user)
options = {} options = {}
options[:committer] = committer options[:committer] = committer
...@@ -913,7 +913,7 @@ class Repository ...@@ -913,7 +913,7 @@ class Repository
end end
def remove_file(user, path, message, branch) 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) committer = user_to_committer(user)
options = {} options = {}
options[:committer] = committer options[:committer] = committer
...@@ -979,7 +979,7 @@ class Repository ...@@ -979,7 +979,7 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit) merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts? 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( actual_options = options.merge(
parents: [our_commit, their_commit], parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged), tree: merge_index.write_tree(rugged),
...@@ -997,7 +997,7 @@ class Repository ...@@ -997,7 +997,7 @@ class Repository
return false unless revert_tree_id 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) committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged, source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message, message: commit.revert_message,
...@@ -1014,7 +1014,7 @@ class Repository ...@@ -1014,7 +1014,7 @@ class Repository
return false unless cherry_pick_tree_id 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) committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged, source_sha = Rugged::Commit.create(rugged,
message: commit.message, message: commit.message,
...@@ -1030,7 +1030,7 @@ class Repository ...@@ -1030,7 +1030,7 @@ class Repository
end end
def resolve_conflicts(user, branch, params) def resolve_conflicts(user, branch, params)
commit_with_hooks(user, branch) do update_branch_with_hooks(user, branch) do
committer = user_to_committer(user) committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
...@@ -1233,7 +1233,7 @@ class Repository ...@@ -1233,7 +1233,7 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
end end
def commit_with_hooks(current_user, branch) def update_branch_with_hooks(current_user, branch)
update_autocrlf_option update_autocrlf_option
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
......
This diff is collapsed.
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .sidebar-wrapper.nicescroll
.sidebar-action-buttons .sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%small= number_to_human_size @blob.size %small= number_to_human_size @blob.size
.file-actions .file-actions
= render "projects/blob/actions" = render "projects/blob/actions"
.file-content.blame.code.js-syntax-highlight .table-responsive.file-content.blame.code.js-syntax-highlight
%table %table
- current_line = 1 - current_line = 1
- @blame_groups.each do |blame_group| - @blame_groups.each do |blame_group|
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
%td.blame-commit %td.blame-commit
.commit .commit
- commit = blame_group[:commit] - commit = blame_group[:commit]
= author_avatar(commit, size: 36)
.commit-row-title .commit-row-title
%strong %strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" = 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) - if can?(current_user, :push_code, @project)
.pull-right .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 = link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
= icon('spinner spin') = icon('spinner spin')
Checking branches Checking branches
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
.tab-content#diff-notes-app .tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes #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 = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row .row
......
- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff - if @merge_request_diffs.size > 1
.mr-version-controls
- if merge_request_diffs.size > 1 Changes between
.mr-version-switch %span.dropdown.inline.mr-version-dropdown
Version:
%span.dropdown.inline
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} } %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
%strong.monospace< %strong
- if @merge_request_diff.latest? - if @merge_request_diff.latest?
#{"latest"} latest version
- else - else
#{@merge_request_diff.head_commit.short_id} version #{version_index(@merge_request_diff)}
%span.caret %span.caret
%ul.dropdown-menu.dropdown-menu-selectable %ul.dropdown-menu.dropdown-menu-selectable
- merge_request_diffs.each do |merge_request_diff| - @merge_request_diffs.each do |merge_request_diff|
%li %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 = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%strong.monospace %strong
#{merge_request_diff.head_commit.short_id} - if merge_request_diff.latest?
%br latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small %small
#{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
= time_ago_with_tooltip(merge_request_diff.created_at) = time_ago_with_tooltip(merge_request_diff.created_at)
- unless @merge_request_diff.latest? - if @merge_request_diff.base_commit_sha
%span.prepend-left-default 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') = icon('info-circle')
This version is not the latest one. Comments are disabled - if @start_sha
.pull-right Comments are disabled because you're comparing two versions of this merge request.
%span.monospace - else
#{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id} Comments are disabled because you're viewing an old version of this merge request.
...@@ -38,6 +38,9 @@ ...@@ -38,6 +38,9 @@
%a{href: "#", data: { id: weight }, class: ("is-active" if params[:weight] == weight.to_s)} %a{href: "#", data: { id: weight }, class: ("is-active" if params[:weight] == weight.to_s)}
= weight = 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 .pull-right
- if controller.controller_name == 'boards' - if controller.controller_name == 'boards'
#js-boards-seach.issue-boards-search #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 ...@@ -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'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *' 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['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 # GitLab Shell
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -42,8 +42,9 @@ following locations: ...@@ -42,8 +42,9 @@ following locations:
- [Sidekiq metrics](sidekiq_metrics.md) - [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md) - [System Hooks](system_hooks.md)
- [Tags](tags.md) - [Tags](tags.md)
- [Users](users.md)
- [Todos](todos.md) - [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
### Internal CI API ### 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 ...@@ -110,8 +110,8 @@ POST /projects/:id/members
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY | | `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash ```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" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
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/projects/:id/members
``` ```
Example response: Example response:
......
...@@ -518,7 +518,7 @@ invalid, 400 is returned. ...@@ -518,7 +518,7 @@ invalid, 400 is returned.
### Fork project ### 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 POST /projects/fork/:id
...@@ -527,6 +527,7 @@ POST /projects/fork/:id ...@@ -527,6 +527,7 @@ POST /projects/fork/:id
Parameters: Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked - `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 ### Star a project
......
...@@ -76,8 +76,8 @@ export CI_RUNNER_DESCRIPTION="my runner" ...@@ -76,8 +76,8 @@ export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux" export CI_RUNNER_TAGS="docker, linux"
export CI_SERVER="yes" export CI_SERVER="yes"
export CI_SERVER_NAME="GitLab" export CI_SERVER_NAME="GitLab"
export CI_SERVER_REVISION="8.9.0" export CI_SERVER_REVISION="70606bf"
export CI_SERVER_VERSION="70606bf" export CI_SERVER_VERSION="8.9.0"
``` ```
### YAML-defined variables ### YAML-defined variables
......
...@@ -86,7 +86,7 @@ if your available memory changes. ...@@ -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. 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 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 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. ...@@ -12,6 +12,12 @@ can select an older one from version dropdown.
![Merge Request Versions](img/versions.png) ![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:** >**Note:**
......
...@@ -49,6 +49,7 @@ module API ...@@ -49,6 +49,7 @@ module API
mount ::API::LicenseTemplates mount ::API::LicenseTemplates
mount ::API::Ldap mount ::API::Ldap
mount ::API::LdapGroupLinks mount ::API::LdapGroupLinks
mount ::API::Lint
mount ::API::Members mount ::API::Members
mount ::API::MergeRequests mount ::API::MergeRequests
mount ::API::Milestones 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 ...@@ -13,11 +13,14 @@ module API
params do params do
optional :page, type: Integer, desc: 'Page number of the current request' optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page' 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 end
get ':id/pipelines' do get ':id/pipelines' do
authorize! :read_pipeline, user_project 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 end
desc 'Gets a specific pipeline for the project' do desc 'Gets a specific pipeline for the project' do
......
...@@ -193,16 +193,30 @@ module API ...@@ -193,16 +193,30 @@ module API
end end
end end
# Fork new project for the current user. # Fork new project for the current user or provided namespace.
# #
# Parameters: # Parameters:
# id (required) - The ID of a project # 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 # Example Request
# POST /projects/fork/:id # POST /projects/fork/:id
post 'fork/:id' do 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 = @forked_project =
::Projects::ForkService.new(user_project, ::Projects::ForkService.new(user_project,
current_user).execute current_user,
attrs).execute
if @forked_project.errors.any? if @forked_project.errors.any?
conflict!(@forked_project.errors.messages) conflict!(@forked_project.errors.messages)
else else
......
...@@ -12,7 +12,7 @@ module Ci ...@@ -12,7 +12,7 @@ module Ci
# POST /builds/register # POST /builds/register
post "register" do post "register" do
authenticate_runner! authenticate_runner!
update_runner_last_contact update_runner_last_contact(save: false)
update_runner_info update_runner_info
required_attributes! [:token] required_attributes! [:token]
not_found! unless current_runner.active? not_found! unless current_runner.active?
......
...@@ -3,7 +3,7 @@ module Ci ...@@ -3,7 +3,7 @@ module Ci
module Helpers module Helpers
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN" BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
BUILD_TOKEN_PARAM = :token BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 60 UPDATE_RUNNER_EVERY = 40 * 60
def authenticate_runners! def authenticate_runners!
forbidden! unless runner_registration_token_valid? forbidden! unless runner_registration_token_valid?
...@@ -22,11 +22,13 @@ module Ci ...@@ -22,11 +22,13 @@ module Ci
params[:token] == current_application_settings.runners_registration_token params[:token] == current_application_settings.runners_registration_token
end end
def update_runner_last_contact def update_runner_last_contact(save: true)
# Use a random threshold to prevent beating DB updates # 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) 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 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
end end
......
...@@ -55,12 +55,7 @@ module Ci ...@@ -55,12 +55,7 @@ module Ci
{ {
stage_idx: @stages.index(job[:stage]), stage_idx: @stages.index(job[:stage]),
stage: job[:stage], stage: job[:stage],
## commands: job[:commands],
# 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"),
tag_list: job[:tags] || [], tag_list: job[:tags] || [],
name: job[:name].to_s, name: job[:name].to_s,
allow_failure: job[:allow_failure] || false, allow_failure: job[:allow_failure] || false,
...@@ -68,16 +63,27 @@ module Ci ...@@ -68,16 +63,27 @@ module Ci
environment: job[:environment], environment: job[:environment],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
options: { options: {
image: job[:image] || @image, image: job[:image],
services: job[:services] || @services, services: job[:services],
artifacts: job[:artifacts], artifacts: job[:artifacts],
cache: job[:cache] || @cache, cache: job[:cache],
dependencies: job[:dependencies], dependencies: job[:dependencies],
after_script: job[:after_script] || @after_script, after_script: job[:after_script],
}.compact }.compact
} }
end 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 private
def initial_parsing def initial_parsing
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
private private
def gl_user_id(project, bitbucket_id) def gitlab_user_id(project, bitbucket_id)
if bitbucket_id if bitbucket_id
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
(user && user.id) || project.creator_id (user && user.id) || project.creator_id
...@@ -74,7 +74,7 @@ module Gitlab ...@@ -74,7 +74,7 @@ module Gitlab
description: body, description: body,
title: issue["title"], title: issue["title"],
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', 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 end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
@config = Loader.new(config).load! @config = Loader.new(config).load!
@global = Node::Global.new(@config) @global = Node::Global.new(@config)
@global.process! @global.compose!
end end
def valid? def valid?
......
...@@ -23,9 +23,9 @@ module Gitlab ...@@ -23,9 +23,9 @@ module Gitlab
end end
end end
private def compose!(deps = nil)
return unless valid?
def compose!
self.class.nodes.each do |key, factory| self.class.nodes.each do |key, factory|
factory factory
.value(@config[key]) .value(@config[key])
...@@ -33,6 +33,12 @@ module Gitlab ...@@ -33,6 +33,12 @@ module Gitlab
@entries[key] = factory.create! @entries[key] = factory.create!
end end
yield if block_given?
@entries.each_value do |entry|
entry.compose!(deps)
end
end end
class_methods do class_methods do
......
...@@ -20,11 +20,14 @@ module Gitlab ...@@ -20,11 +20,14 @@ module Gitlab
@validator.validate(:new) @validator.validate(:new)
end end
def process! def [](key)
@entries[key] || Node::Undefined.new
end
def compose!(deps = nil)
return unless valid? return unless valid?
compose! yield if block_given?
descendants.each(&:process!)
end end
def leaf? def leaf?
...@@ -73,11 +76,6 @@ module Gitlab ...@@ -73,11 +76,6 @@ module Gitlab
def self.validator def self.validator
Validator Validator
end end
private
def compose!
end
end end
end end
end end
......
...@@ -37,8 +37,8 @@ module Gitlab ...@@ -37,8 +37,8 @@ module Gitlab
# See issue #18775. # See issue #18775.
# #
if @value.nil? if @value.nil?
Node::Undefined.new( Node::Unspecified.new(
fabricate_undefined fabricate_unspecified
) )
else else
fabricate(@node, @value) fabricate(@node, @value)
...@@ -47,13 +47,13 @@ module Gitlab ...@@ -47,13 +47,13 @@ module Gitlab
private private
def fabricate_undefined def fabricate_unspecified
## ##
# If node has a default value we fabricate concrete node # If node has a default value we fabricate concrete node
# with default value. # with default value.
# #
if @node.default.nil? if @node.default.nil?
fabricate(Node::Null) fabricate(Node::Undefined)
else else
fabricate(@node, @node.default) fabricate(@node, @node.default)
end end
......
...@@ -36,15 +36,15 @@ module Gitlab ...@@ -36,15 +36,15 @@ module Gitlab
helpers :before_script, :image, :services, :after_script, helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs :variables, :stages, :types, :cache, :jobs
private def compose!(_deps = nil)
super(self) do
def compose! compose_jobs!
super compose_deprecated_entries!
end
compose_jobs!
compose_deprecated_entries!
end end
private
def compose_jobs! def compose_jobs!
factory = Node::Factory.new(Node::Jobs) factory = Node::Factory.new(Node::Jobs)
.value(@config.except(*self.class.nodes.keys)) .value(@config.except(*self.class.nodes.keys))
......
...@@ -80,7 +80,19 @@ module Gitlab ...@@ -80,7 +80,19 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script, helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables, :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 def name
@metadata[:name] @metadata[:name]
...@@ -90,12 +102,30 @@ module Gitlab ...@@ -90,12 +102,30 @@ module Gitlab
@config.merge(to_hash.compact) @config.merge(to_hash.compact)
end end
def commands
(before_script_value.to_a + script_value.to_a).join("\n")
end
private 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 def to_hash
{ name: name, { name: name,
before_script: before_script, before_script: before_script,
script: script, script: script,
commands: commands,
image: image, image: image,
services: services, services: services,
stage: stage, stage: stage,
...@@ -106,16 +136,6 @@ module Gitlab ...@@ -106,16 +136,6 @@ module Gitlab
artifacts: artifacts, artifacts: artifacts,
after_script: after_script } after_script: after_script }
end end
def compose!
super
if type_defined? && !stage_defined?
@entries[:stage] = @entries[:type]
end
@entries.delete(:type)
end
end end
end end
end end
......
...@@ -26,19 +26,23 @@ module Gitlab ...@@ -26,19 +26,23 @@ module Gitlab
name.to_s.start_with?('.') name.to_s.start_with?('.')
end end
private def compose!(deps = nil)
super do
def compose! @config.each do |name, config|
@config.each do |name, config| node = hidden?(name) ? Node::Hidden : Node::Job
node = hidden?(name) ? Node::Hidden : Node::Job
factory = Node::Factory.new(node)
factory = Node::Factory.new(node) .value(config || {})
.value(config || {}) .metadata(name: name)
.metadata(name: name) .with(key: name, parent: self,
.with(key: name, parent: self, description: "#{name} job definition.")
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 end
end end
......
...@@ -3,15 +3,34 @@ module Gitlab ...@@ -3,15 +3,34 @@ module Gitlab
class Config class Config
module Node 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 # Implements the Null Object pattern.
# unspecified.
# #
class Undefined < SimpleDelegator class Undefined < Entry
def initialize(*)
super(nil)
end
def value
nil
end
def valid?
true
end
def errors
[]
end
def specified? def specified?
false false
end end
def relevant?
false
end
end end
end end
end end
......
...@@ -3,30 +3,15 @@ module Gitlab ...@@ -3,30 +3,15 @@ module Gitlab
class Config class Config
module Node 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 class Unspecified < SimpleDelegator
def value
nil
end
def valid?
true
end
def errors
[]
end
def specified? def specified?
false false
end end
def relevant?
false
end
end end
end end
end end
......
...@@ -18,7 +18,7 @@ module Gitlab ...@@ -18,7 +18,7 @@ module Gitlab
def parse(text, our_path:, their_path:, parent_file: nil) def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 102400 raise UnmergeableFile if text.length > 200.kilobytes
begin begin
text.to_json text.to_json
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
private private
def gl_user_id(github_id) def gitlab_user_id(github_id)
User.joins(:identities). User.joins(:identities).
find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
try(:id) try(:id)
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
end end
def author_id def author_id
gl_user_id(raw_data.user.id) || project.creator_id gitlab_user_id(raw_data.user.id) || project.creator_id
end end
def body def body
......
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
def assignee_id def assignee_id
if assigned? if assigned?
gl_user_id(raw_data.assignee.id) gitlab_user_id(raw_data.assignee.id)
end end
end end
...@@ -49,7 +49,7 @@ module Gitlab ...@@ -49,7 +49,7 @@ module Gitlab
end end
def author_id def author_id
gl_user_id(raw_data.user.id) || project.creator_id gitlab_user_id(raw_data.user.id) || project.creator_id
end end
def body def body
......
...@@ -68,7 +68,7 @@ module Gitlab ...@@ -68,7 +68,7 @@ module Gitlab
def assignee_id def assignee_id
if assigned? if assigned?
gl_user_id(raw_data.assignee.id) gitlab_user_id(raw_data.assignee.id)
end end
end end
...@@ -77,7 +77,7 @@ module Gitlab ...@@ -77,7 +77,7 @@ module Gitlab
end end
def author_id def author_id
gl_user_id(raw_data.user.id) || project.creator_id gitlab_user_id(raw_data.user.id) || project.creator_id
end end
def body def body
......
...@@ -41,7 +41,8 @@ module Gitlab ...@@ -41,7 +41,8 @@ module Gitlab
title: issue["title"], title: issue["title"],
state: issue["state"], state: issue["state"],
updated_at: issue["updated_at"], 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
end end
...@@ -51,7 +52,7 @@ module Gitlab ...@@ -51,7 +52,7 @@ module Gitlab
private 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.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s)
(user && user.id) || project.creator_id (user && user.id) || project.creator_id
end end
......
...@@ -28,6 +28,11 @@ FactoryGirl.define do ...@@ -28,6 +28,11 @@ FactoryGirl.define do
diff_refs: noteable.diff_refs diff_refs: noteable.diff_refs
) )
end end
trait :resolved do
resolved_at { Time.now }
resolved_by { create(:user) }
end
end end
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do 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 ...@@ -11,8 +11,8 @@ feature 'Merge Request versions', js: true, feature: true do
end end
it 'show the latest version of the diff' do it 'show the latest version of the diff' do
page.within '.mr-version-switch' do page.within '.mr-version-dropdown' do
expect(page).to have_content 'Version: latest' expect(page).to have_content 'latest version'
end end
expect(page).to have_content '8 changed files' expect(page).to have_content '8 changed files'
...@@ -20,18 +20,49 @@ feature 'Merge Request versions', js: true, feature: true do ...@@ -20,18 +20,49 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'switch between versions' do describe 'switch between versions' do
before do before do
page.within '.mr-version-switch' do page.within '.mr-version-dropdown' do
find('.btn-link').click find('.btn-link').click
click_link '6f6d7e7e' click_link 'version 1'
end end
end end
it 'should show older version' do it 'should show older version' do
page.within '.mr-version-switch' do page.within '.mr-version-dropdown' do
expect(page).to have_content 'Version: 6f6d7e7e' expect(page).to have_content 'version 1'
end end
expect(page).to have_content '5 changed files' expect(page).to have_content '5 changed files'
end 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
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 ...@@ -183,4 +183,48 @@ describe ProjectsHelper do
end end
end 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 end
...@@ -32,6 +32,10 @@ describe SearchHelper do ...@@ -32,6 +32,10 @@ describe SearchHelper do
expect(search_autocomplete_opts("adm").size).to eq(1) expect(search_autocomplete_opts("adm").size).to eq(1)
end 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 it "includes the user's groups" do
create(:group).add_owner(user) create(:group).add_owner(user)
expect(search_autocomplete_opts("gro").size).to eq(1) 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 notes */
/*= require autosize */
/*= require gl_form */ /*= require gl_form */
/*= require lib/utils/text_utility */
(function() { (function() {
window.gon || (window.gon = {}); window.gon || (window.gon = {});
...@@ -12,29 +11,63 @@ ...@@ -12,29 +11,63 @@
}; };
describe('Notes', function() { describe('Notes', function() {
return describe('task lists', function() { describe('task lists', function() {
fixture.preload('issue_note.html'); fixture.preload('issue_note.html');
beforeEach(function() { beforeEach(function() {
fixture.load('issue_note.html'); fixture.load('issue_note.html');
$('form').on('submit', function(e) { $('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() { it('modifies the Markdown field', function() {
$('input[type=checkbox]').attr('checked', true).trigger('change'); $('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; it('submits the form on tasklist:changed', function() {
submitted = false; var submitted = false;
$('form').on('submit', function(e) { $('form').on('submit', function(e) {
submitted = true; submitted = true;
return e.preventDefault(); e.preventDefault();
}); });
$('.js-task-list-field').trigger('tasklist:changed'); $('.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 ...@@ -1250,5 +1250,40 @@ EOT
end end
end 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
end end
...@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do ...@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validations' do describe 'validations' do
before { entry.process! } before { entry.compose! }
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:config) do let(:config) do
......
...@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do ...@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do
.value(nil) .value(nil)
.create! .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
end end
......
...@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do
end end
context 'when hash is valid' do context 'when hash is valid' do
context 'when all entries defined' do context 'when some entries defined' do
let(:hash) do let(:hash) do
{ before_script: ['ls', 'pwd'], { before_script: ['ls', 'pwd'],
image: 'ruby:2.2', image: 'ruby:2.2',
...@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do
stages: ['build', 'pages'], stages: ['build', 'pages'],
cache: { key: 'k', untracked: true, paths: ['public/'] }, cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] }, rspec: { script: %w[rspec ls] },
spinach: { script: 'spinach' } } spinach: { before_script: [], variables: {}, script: 'spinach' } }
end end
describe '#process!' do describe '#compose!' do
before { global.process! } before { global.compose! }
it 'creates nodes hash' do it 'creates nodes hash' do
expect(global.descendants).to be_an Array expect(global.descendants).to be_an Array
...@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do
end end
end end
context 'when not processed' do context 'when not composed' do
describe '#before_script' do describe '#before_script' do
it 'returns nil' do it 'returns nil' do
expect(global.before_script).to be nil expect(global.before_script).to be nil
...@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do
end end
end end
context 'when processed' do context 'when composed' do
before { global.process! } before { global.compose! }
describe '#errors' do
it 'has no errors' do
expect(global.errors).to be_empty
end
end
describe '#before_script' do describe '#before_script' do
it 'returns correct script' do it 'returns correct script' do
...@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.jobs).to eq( expect(global.jobs).to eq(
rspec: { name: :rspec, rspec: { name: :rspec,
script: %w[rspec ls], 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, spinach: { name: :spinach,
before_script: [],
script: %w[spinach], 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
end end
...@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do
end end
context 'when most of entires not defined' do context 'when most of entires not defined' do
let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } before { global.compose! }
before { global.process! }
let(:hash) do
{ cache: { key: 'a' }, rspec: { script: %w[ls] } }
end
describe '#nodes' do describe '#nodes' do
it 'instantizes all nodes' do it 'instantizes all nodes' do
expect(global.descendants.count).to eq 8 expect(global.descendants.count).to eq 8
end end
it 'contains undefined nodes' do it 'contains unspecified nodes' do
expect(global.descendants.first) 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
end end
...@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do
# details. # details.
# #
context 'when entires specified but not defined' do context 'when entires specified but not defined' do
let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } before { global.compose! }
before { global.process! }
let(:hash) do
{ variables: nil, rspec: { script: 'rspec' } }
end
describe '#variables' do describe '#variables' do
it 'undefined entry returns a default value' do it 'undefined entry returns a default value' do
...@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do
end end
context 'when hash is not valid' do context 'when hash is not valid' do
before { global.process! } before { global.compose! }
let(:hash) do let(:hash) do
{ before_script: 'ls' } { before_script: 'ls' }
...@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do ...@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.specified?).to be true expect(global.specified?).to be true
end end
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 end
...@@ -3,9 +3,9 @@ require 'spec_helper' ...@@ -3,9 +3,9 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Job do describe Gitlab::Ci::Config::Node::Job do
let(:entry) { described_class.new(config, name: :rspec) } let(:entry) { described_class.new(config, name: :rspec) }
before { entry.process! }
describe 'validations' do describe 'validations' do
before { entry.compose! }
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:config) { { script: 'rspec' } } let(:config) { { script: 'rspec' } }
...@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do ...@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do
end end
end end
describe '#value' do describe '#relevant?' do
context 'when entry is correct' 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 let(:config) do
{ before_script: %w[ls pwd], { image: 'some_image', cache: { key: 'test' } }
script: 'rspec', end
after_script: %w[cleanup] }
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 end
it 'returns correct value' do let(:config) { { script: 'ls', cache: { key: 'test' } } }
expect(entry.value)
.to eq(name: :rspec, it 'uses config from global entry' do
before_script: %w[ls pwd], expect(entry[:image].value).to eq 'specified'
script: %w[rspec], expect(entry[:cache].value).to eq(key: 'test')
stage: 'test',
after_script: %w[cleanup])
end end
end end
end end
describe '#relevant?' do context 'when composed' do
it 'is a relevant entry' do before { entry.compose! }
expect(entry).to be_relevant
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 end
end end
...@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do ...@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validations' do describe 'validations' do
before { entry.process! } before { entry.compose! }
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:config) { { rspec: { script: 'rspec' } } } let(:config) { { rspec: { script: 'rspec' } } }
...@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do ...@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do
end end
end end
context 'when valid job entries processed' do context 'when valid job entries composed' do
before { entry.process! } before { entry.compose! }
let(:config) do let(:config) do
{ rspec: { script: 'rspec' }, { rspec: { script: 'rspec' },
...@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do ...@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.value).to eq( expect(entry.value).to eq(
rspec: { name: :rspec, rspec: { name: :rspec,
script: %w[rspec], script: %w[rspec],
commands: 'rspec',
stage: 'test' }, stage: 'test' },
spinach: { name: :spinach, spinach: { name: :spinach,
script: %w[spinach], script: %w[spinach],
commands: 'spinach',
stage: 'test' }) stage: 'test' })
end end
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' ...@@ -3,9 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe '#process!' do describe 'validations' do
before { entry.process! }
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:config) { ['ls', 'pwd'] } let(:config) { ['ls', 'pwd'] }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do describe Gitlab::Ci::Config::Node::Undefined do
let(:undefined) { described_class.new(entry) } let(:entry) { described_class.new }
let(:entry) { spy('Entry') }
describe '#leaf?' do
it 'is leaf node' do
expect(entry).to be_leaf
end
end
describe '#valid?' do describe '#valid?' do
it 'delegates method to entry' do it 'is always valid' do
expect(undefined.valid).to eq entry expect(entry).to be_valid
end end
end end
describe '#errors' do describe '#errors' do
it 'delegates method to entry' do it 'is does not contain errors' do
expect(undefined.errors).to eq entry expect(entry.errors).to be_empty
end end
end end
describe '#value' do describe '#value' do
it 'delegates method to entry' do it 'returns nil' do
expect(undefined.value).to eq entry expect(entry.value).to eq nil
end end
end end
describe '#specified?' do describe '#relevant?' do
it 'is always false' do it 'is not relevant' do
allow(entry).to receive(:specified?).and_return(true) 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 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 ...@@ -179,8 +179,8 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end end
it 'raises UnmergeableFile when the file is over 100 KB' do it 'raises UnmergeableFile when the file is over 200 KB' do
expect { parse_text('a' * 102401) }. expect { parse_text('a' * 204801) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end end
......
...@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do ...@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
'title' => 'Issue', 'title' => 'Issue',
'description' => 'Lorem ipsum', 'description' => 'Lorem ipsum',
'state' => 'opened', 'state' => 'opened',
'confidential' => true,
'author' => { 'author' => {
'id' => 283999, 'id' => 283999,
'name' => 'John Doe' 'name' => 'John Doe'
...@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do ...@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
title: 'Issue', title: 'Issue',
description: "*Created by: John Doe*\n\nLorem ipsum", description: "*Created by: John Doe*\n\nLorem ipsum",
state: 'opened', state: 'opened',
confidential: true,
author_id: project.creator_id author_id: project.creator_id
} }
......
...@@ -31,6 +31,43 @@ describe DiffNote, models: true do ...@@ -31,6 +31,43 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } 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 describe "#position=" do
context "when provided a string" do context "when provided a string" do
it "sets the position" do it "sets the position" do
......
...@@ -238,27 +238,19 @@ describe Discussion, model: true do ...@@ -238,27 +238,19 @@ describe Discussion, model: true do
context "when resolvable" do context "when resolvable" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:second_note) { create(:diff_note_on_commit) } # unresolvable
before do before do
allow(subject).to receive(:resolvable?).and_return(true) 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 end
context "when all resolvable notes are resolved" do context "when all resolvable notes are resolved" do
before do before do
first_note.resolve!(user) first_note.resolve!(user)
third_note.resolve!(user) third_note.resolve!(user)
end
it "calls resolve! on every resolvable note" do first_note.reload
expect(first_note).to receive(:resolve!).with(current_user) third_note.reload
expect(second_note).not_to receive(:resolve!)
expect(third_note).to receive(:resolve!).with(current_user)
subject.resolve!(current_user)
end end
it "doesn't change resolved_at on the resolved notes" do it "doesn't change resolved_at on the resolved notes" do
...@@ -309,46 +301,44 @@ describe Discussion, model: true do ...@@ -309,46 +301,44 @@ describe Discussion, model: true do
first_note.resolve!(user) first_note.resolve!(user)
end 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 it "doesn't change resolved_at on the resolved note" do
expect(first_note.resolved_at).not_to be_nil 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 end
it "doesn't change resolved_by on the resolved note" do it "doesn't change resolved_by on the resolved note" do
expect(first_note.resolved_by).to eq(user) 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 end
it "doesn't change the resolved state on the resolved note" do it "doesn't change the resolved state on the resolved note" do
expect(first_note.resolved?).to be true 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 end
it "sets resolved_at on the unresolved note" do it "sets resolved_at on the unresolved note" do
subject.resolve!(current_user) subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved_at).not_to be_nil expect(third_note.resolved_at).not_to be_nil
end end
it "sets resolved_by on the unresolved note" do it "sets resolved_by on the unresolved note" do
subject.resolve!(current_user) subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved_by).to eq(current_user) expect(third_note.resolved_by).to eq(current_user)
end end
it "marks the unresolved note as resolved" do it "marks the unresolved note as resolved" do
subject.resolve!(current_user) subject.resolve!(current_user)
third_note.reload
expect(third_note.resolved?).to be true expect(third_note.resolved?).to be true
end end
...@@ -373,16 +363,10 @@ describe Discussion, model: true do ...@@ -373,16 +363,10 @@ describe Discussion, model: true do
end end
context "when no resolvable notes are resolved" do 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 it "sets resolved_at on the unresolved notes" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved_at).not_to be_nil expect(first_note.resolved_at).not_to be_nil
expect(third_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 ...@@ -390,6 +374,8 @@ describe Discussion, model: true do
it "sets resolved_by on the unresolved notes" do it "sets resolved_by on the unresolved notes" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved_by).to eq(current_user) expect(first_note.resolved_by).to eq(current_user)
expect(third_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 ...@@ -397,6 +383,8 @@ describe Discussion, model: true do
it "marks the unresolved notes as resolved" do it "marks the unresolved notes as resolved" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(first_note.resolved?).to be true expect(first_note.resolved?).to be true
expect(third_note.resolved?).to be true expect(third_note.resolved?).to be true
...@@ -404,18 +392,24 @@ describe Discussion, model: true do ...@@ -404,18 +392,24 @@ describe Discussion, model: true do
it "sets resolved_at" do it "sets resolved_at" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved_at).not_to be_nil expect(subject.resolved_at).not_to be_nil
end end
it "sets resolved_by" do it "sets resolved_by" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved_by).to eq(current_user) expect(subject.resolved_by).to eq(current_user)
end end
it "marks as resolved" do it "marks as resolved" do
subject.resolve!(current_user) subject.resolve!(current_user)
first_note.reload
third_note.reload
expect(subject.resolved?).to be true expect(subject.resolved?).to be true
end end
...@@ -451,16 +445,10 @@ describe Discussion, model: true do ...@@ -451,16 +445,10 @@ describe Discussion, model: true do
third_note.resolve!(user) third_note.resolve!(user)
end 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 it "unsets resolved_at on the resolved notes" do
subject.unresolve! subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved_at).to be_nil expect(first_note.resolved_at).to be_nil
expect(third_note.resolved_at).to be_nil expect(third_note.resolved_at).to be_nil
...@@ -468,6 +456,8 @@ describe Discussion, model: true do ...@@ -468,6 +456,8 @@ describe Discussion, model: true do
it "unsets resolved_by on the resolved notes" do it "unsets resolved_by on the resolved notes" do
subject.unresolve! subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved_by).to be_nil expect(first_note.resolved_by).to be_nil
expect(third_note.resolved_by).to be_nil expect(third_note.resolved_by).to be_nil
...@@ -475,6 +465,8 @@ describe Discussion, model: true do ...@@ -475,6 +465,8 @@ describe Discussion, model: true do
it "unmarks the resolved notes as resolved" do it "unmarks the resolved notes as resolved" do
subject.unresolve! subject.unresolve!
first_note.reload
third_note.reload
expect(first_note.resolved?).to be false expect(first_note.resolved?).to be false
expect(third_note.resolved?).to be false expect(third_note.resolved?).to be false
...@@ -482,12 +474,16 @@ describe Discussion, model: true do ...@@ -482,12 +474,16 @@ describe Discussion, model: true do
it "unsets resolved_at" do it "unsets resolved_at" do
subject.unresolve! subject.unresolve!
first_note.reload
third_note.reload
expect(subject.resolved_at).to be_nil expect(subject.resolved_at).to be_nil
end end
it "unsets resolved_by" do it "unsets resolved_by" do
subject.unresolve! subject.unresolve!
first_note.reload
third_note.reload
expect(subject.resolved_by).to be_nil expect(subject.resolved_by).to be_nil
end end
...@@ -504,40 +500,22 @@ describe Discussion, model: true do ...@@ -504,40 +500,22 @@ describe Discussion, model: true do
first_note.resolve!(user) first_note.resolve!(user)
end 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 it "unsets resolved_at on the resolved note" do
subject.unresolve! subject.unresolve!
expect(first_note.resolved_at).to be_nil expect(subject.first_note.resolved_at).to be_nil
end end
it "unsets resolved_by on the resolved note" do it "unsets resolved_by on the resolved note" do
subject.unresolve! subject.unresolve!
expect(first_note.resolved_by).to be_nil expect(subject.first_note.resolved_by).to be_nil
end end
it "unmarks the resolved note as resolved" do it "unmarks the resolved note as resolved" do
subject.unresolve! subject.unresolve!
expect(first_note.resolved?).to be false expect(subject.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!
end end
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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