Commit 0ac012f0 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge remote-tracking branch 'origin/master' into ide

parents 933cfe1e 25190274
...@@ -323,7 +323,7 @@ GEM ...@@ -323,7 +323,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
retriable (~> 1.4) retriable (~> 1.4)
signet (~> 0.6) signet (~> 0.6)
google-protobuf (3.2.0.2) google-protobuf (3.3.0)
googleauth (0.5.1) googleauth (0.5.1)
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.4) jwt (~> 1.4)
......
...@@ -128,7 +128,7 @@ information, see ...@@ -128,7 +128,7 @@ information, see
### After the 7th ### After the 7th
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for [regressions](#regressions)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
These fixes will be shipped in the next RC for that release if it is before the 22nd. These fixes will be shipped in the next RC for that release if it is before the 22nd.
...@@ -158,6 +158,24 @@ release should have the correct milestone assigned _and_ have the label ...@@ -158,6 +158,24 @@ release should have the correct milestone assigned _and_ have the label
Merge requests without a milestone and this label will Merge requests without a milestone and this label will
not be merged into any stable branches. not be merged into any stable branches.
### Regressions
A regression for a particular monthly release is a bug that exists in that
release, but wasn't present in the release before. This includes bugs in
features that were only added in that monthly release. Every regression **must**
have the milestone of the release it was introduced in - if a regression doesn't
have a milestone, it might be 'just' a bug!
For instance, if 10.5.0 adds a feature, and that feature doesn't work correctly,
then this is a regression in 10.5. If 10.5.1 then fixes that, but 10.5.3 somehow
reintroduces the bug, then this bug is still a regression in 10.5.
Because GitLab.com runs release candidates of new releases, a regression can be
reported in a release before its 'official' release date on the 22nd of the
month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version
available in the package repositories.
## Release retrospective and kickoff ## Release retrospective and kickoff
### Retrospective ### Retrospective
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
/* global LabelsSelect */ /* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global Commit */ /* global Commit */
/* global NewBranchForm */
/* global NotificationsForm */ /* global NotificationsForm */
/* global NotificationsDropdown */ /* global NotificationsDropdown */
/* global GroupAvatar */ /* global GroupAvatar */
...@@ -263,7 +264,7 @@ import FeatureHelper from './helpers/feature_helper'; ...@@ -263,7 +264,7 @@ import FeatureHelper from './helpers/feature_helper';
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
new gl.GLForm($('.tag-form'), true); new gl.GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); new RefSelectDropdown($('.js-branch-select'));
break; break;
case 'projects:snippets:show': case 'projects:snippets:show':
initNotes(); initNotes();
...@@ -334,6 +335,9 @@ import FeatureHelper from './helpers/feature_helper'; ...@@ -334,6 +335,9 @@ import FeatureHelper from './helpers/feature_helper';
case 'projects:edit': case 'projects:edit':
setupProjectEdit(); setupProjectEdit();
break; break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:failures': case 'projects:pipelines:failures':
case 'projects:pipelines:show': case 'projects:pipelines:show':
...@@ -390,6 +394,9 @@ import FeatureHelper from './helpers/feature_helper'; ...@@ -390,6 +394,9 @@ import FeatureHelper from './helpers/feature_helper';
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
break; break;
case 'projects:find_file:show': case 'projects:find_file:show':
shortcut_handler = true; shortcut_handler = true;
...@@ -548,6 +555,7 @@ import FeatureHelper from './helpers/feature_helper'; ...@@ -548,6 +555,7 @@ import FeatureHelper from './helpers/feature_helper';
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form'), true); new gl.GLForm($('.wiki-form'), true);
new Sidebar();
break; break;
case 'snippets': case 'snippets':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -529,6 +529,7 @@ export default class Notes { ...@@ -529,6 +529,7 @@ export default class Notes {
form.find('#note_line_code').remove(); form.find('#note_line_code').remove();
form.find('#note_position').remove(); form.find('#note_position').remove();
form.find('#note_type').val(''); form.find('#note_type').val('');
form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove(); form.find('#in_reply_to_discussion_id').remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
this.parentTimeline = form.parents('.timeline'); this.parentTimeline = form.parents('.timeline');
...@@ -556,6 +557,7 @@ export default class Notes { ...@@ -556,6 +557,7 @@ export default class Notes {
form.find('#note_noteable_id').val(), form.find('#note_noteable_id').val(),
form.find('#note_commit_id').val(), form.find('#note_commit_id').val(),
form.find('#note_type').val(), form.find('#note_type').val(),
form.find('#note_project_id').val(),
form.find('#in_reply_to_discussion_id').val(), form.find('#in_reply_to_discussion_id').val(),
// LegacyDiffNote // LegacyDiffNote
...@@ -848,6 +850,8 @@ export default class Notes { ...@@ -848,6 +850,8 @@ export default class Notes {
form.find('#in_reply_to_discussion_id').val(discussionID); form.find('#in_reply_to_discussion_id').val(discussionID);
} }
form.find('#note_project_id').val(dataHolder.data('discussionProjectId'));
form.attr('data-line-code', dataHolder.data('lineCode')); form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType')); form.find('#line_type').val(dataHolder.data('lineType'));
......
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const buildChart = (chartScope) => {
const data = {
labels: chartScope.labels,
datasets: [{
fillColor: '#7f8fa4',
strokeColor: '#7f8fa4',
pointColor: '#7f8fa4',
pointStrokeColor: '#EEE',
data: chartScope.totalValues,
},
{
fillColor: '#44aa22',
strokeColor: '#44aa22',
pointColor: '#44aa22',
pointStrokeColor: '#fff',
data: chartScope.successValues,
},
],
};
const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Line(data, options);
};
chartData.forEach(scope => buildChart(scope));
});
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const data = {
labels: chartData.labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartData.values,
}],
};
const ctx = $('#build_timesChart').get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Bar(data, options);
});
document.addEventListener('DOMContentLoaded', () => {
const importBtnTooltip = 'Please enter a valid project name.';
const $importBtnWrapper = $('.import_gitlab_project');
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
$('.how_to_import_link').next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$('#project_path').val()}`);
});
$('.btn_import_gitlab_project').attr('disabled', !$('#project_path').val().trim().length);
$importBtnWrapper.attr('title', importBtnTooltip);
$('#new_project').on('submit', () => {
const $path = $('#project_path');
$path.val($path.val().trim());
});
$('#project_path').on('keyup', () => {
if ($('#project_path').val().trim().length) {
$('.btn_import_gitlab_project').attr('disabled', false);
$importBtnWrapper.attr('title', '');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled', true);
$importBtnWrapper.addClass('has-tooltip');
}
});
$('#project_import_url').disable();
$('.import_git').on('click', () => {
const $projectImportUrl = $('#project_import_url');
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
});
class RefSelectDropdown { class RefSelectDropdown {
constructor($dropdownButton, availableRefs) { constructor($dropdownButton, availableRefs) {
const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML);
$dropdownButton.glDropdown({ $dropdownButton.glDropdown({
data: availableRefs, data: availableRefsValue,
filterable: true, filterable: true,
filterByText: true, filterByText: true,
remote: false, remote: false,
......
...@@ -65,7 +65,7 @@ export default class MergeRequestStore { ...@@ -65,7 +65,7 @@ export default class MergeRequestStore {
this.mergeCheckPath = data.merge_check_path; this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path; this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened' || data.state === 'reopened' || false; this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path; this.canMerge = !!data.merge_path;
......
...@@ -88,6 +88,10 @@ ...@@ -88,6 +88,10 @@
overflow: hidden; overflow: hidden;
display: flex; display: flex;
a {
display: flex;
}
.avatar { .avatar {
border-radius: 0; border-radius: 0;
border: none; border: none;
......
...@@ -414,13 +414,16 @@ ...@@ -414,13 +414,16 @@
background-color: $dropdown-hover-color; background-color: $dropdown-hover-color;
color: $white-light; color: $white-light;
text-decoration: none; text-decoration: none;
outline: 0;
.avatar { .avatar {
border-color: $white-light; border-color: $white-light;
} }
} }
.filter-dropdown-item { .droplab-dropdown .dropdown-menu .filter-dropdown-item {
padding: 0;
.btn { .btn {
border: none; border: none;
width: 100%; width: 100%;
...@@ -455,14 +458,11 @@ ...@@ -455,14 +458,11 @@
} }
.dropdown-user { .dropdown-user {
display: -webkit-flex;
display: flex; display: flex;
} }
.dropdown-user-details { .dropdown-user-details {
display: -webkit-flex;
display: flex; display: flex;
-webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
> span { > span {
......
...@@ -315,6 +315,10 @@ header { ...@@ -315,6 +315,10 @@ header {
} }
} }
.with-performance-bar header.navbar-gitlab {
top: $performance-bar-height;
}
.navbar-nav { .navbar-nav {
li { li {
.badge { .badge {
......
...@@ -127,3 +127,7 @@ of the body element here, we negate cascading side effects but allow momentum sc ...@@ -127,3 +127,7 @@ of the body element here, we negate cascading side effects but allow momentum sc
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.with-performance-bar .page-with-sidebar {
margin-top: $header-height + $performance-bar-height;
}
...@@ -347,6 +347,10 @@ ...@@ -347,6 +347,10 @@
} }
} }
.with-performance-bar .layout-nav {
margin-top: $header-height + $performance-bar-height;
}
.scrolling-tabs-container { .scrolling-tabs-container {
position: relative; position: relative;
...@@ -441,6 +445,22 @@ ...@@ -441,6 +445,22 @@
} }
} }
.with-performance-bar .page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2 + $performance-bar-height;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3 + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
}
.nav-block { .nav-block {
&.activities { &.activities {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
......
...@@ -89,6 +89,10 @@ ...@@ -89,6 +89,10 @@
} }
} }
.with-performance-bar .right-sidebar.affix {
top: $header-height + $performance-bar-height;
}
@mixin maintain-sidebar-dimensions { @mixin maintain-sidebar-dimensions {
display: block; display: block;
width: $gutter-width; width: $gutter-width;
......
...@@ -205,6 +205,7 @@ $divergence-graph-separator-bg: #ccc; ...@@ -205,6 +205,7 @@ $divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms; $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232); $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
/* /*
......
...@@ -325,6 +325,7 @@ header.navbar-gitlab-new { ...@@ -325,6 +325,7 @@ header.navbar-gitlab-new {
.breadcrumbs-links { .breadcrumbs-links {
flex: 1; flex: 1;
min-width: 0;
align-self: center; align-self: center;
color: $gl-text-color-quaternary; color: $gl-text-color-quaternary;
...@@ -343,7 +344,7 @@ header.navbar-gitlab-new { ...@@ -343,7 +344,7 @@ header.navbar-gitlab-new {
} }
.title { .title {
white-space: nowrap; display: inline-block;
> a { > a {
&:last-of-type:not(:first-child) { &:last-of-type:not(:first-child) {
......
...@@ -118,7 +118,7 @@ $new-sidebar-width: 220px; ...@@ -118,7 +118,7 @@ $new-sidebar-width: 220px;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: left $sidebar-transition-duration; transition: left $sidebar-transition-duration;
top: 50px; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
overflow: auto; overflow: auto;
...@@ -163,6 +163,10 @@ $new-sidebar-width: 220px; ...@@ -163,6 +163,10 @@ $new-sidebar-width: 220px;
} }
} }
.with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height;
}
.sidebar-sub-level-items { .sidebar-sub-level-items {
display: none; display: none;
padding-bottom: 8px; padding-bottom: 8px;
...@@ -260,7 +264,7 @@ $new-sidebar-width: 220px; ...@@ -260,7 +264,7 @@ $new-sidebar-width: 220px;
// Make issue boards full-height now that sub-nav is gone // Make issue boards full-height now that sub-nav is gone
.boards-list { .boards-list {
height: calc(100vh - 50px); height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
...@@ -270,6 +274,10 @@ $new-sidebar-width: 220px; ...@@ -270,6 +274,10 @@ $new-sidebar-width: 220px;
} }
} }
.with-performance-bar .boards-list {
height: calc(100vh - #{$header-height} - #{$performance-bar-height});
}
// Change color of all horizontal tabs to match the new indigo color // Change color of all horizontal tabs to match the new indigo color
.nav-links li.active a { .nav-links li.active a {
......
...@@ -64,10 +64,10 @@ ...@@ -64,10 +64,10 @@
color: $gl-text-color; color: $gl-text-color;
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: 50px; top: $header-height;
&.affix { &.affix {
top: 50px; top: $header-height;
} }
// with sidebar // with sidebar
...@@ -86,6 +86,7 @@ ...@@ -86,6 +86,7 @@
position: absolute; position: absolute;
right: 0; right: 0;
left: 0; left: 0;
top: 0;
} }
.truncated-info { .truncated-info {
...@@ -171,6 +172,16 @@ ...@@ -171,6 +172,16 @@
} }
} }
.with-performance-bar .build-page {
.top-bar {
top: $header-height + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
.build-header { .build-header {
.ci-header-container, .ci-header-container,
.header-action-buttons { .header-action-buttons {
......
...@@ -445,6 +445,14 @@ ...@@ -445,6 +445,14 @@
} }
} }
.with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height;
.issuable-sidebar {
height: calc(100% - #{$header-height} - #{$performance-bar-height});
}
}
.detail-page-description { .detail-page-description {
padding: 16px 0; padding: 16px 0;
......
...@@ -759,6 +759,10 @@ ...@@ -759,6 +759,10 @@
} }
} }
.with-performance-bar .merge-request-tabs-holder {
top: $header-height + $performance-bar-height;
}
.merge-request-tabs { .merge-request-tabs {
display: flex; display: flex;
margin-bottom: 0; margin-bottom: 0;
......
...@@ -3,9 +3,16 @@ ...@@ -3,9 +3,16 @@
@import "peek/views/rblineprof"; @import "peek/views/rblineprof";
#peek { #peek {
height: 35px; position: fixed;
left: 0;
top: 0;
width: 100%;
z-index: 2000;
overflow-x: hidden;
height: $performance-bar-height;
background: $black; background: $black;
line-height: 35px; line-height: $performance-bar-height;
color: $perf-bar-text; color: $perf-bar-text;
&.disabled { &.disabled {
...@@ -25,7 +32,8 @@ ...@@ -25,7 +32,8 @@
} }
.wrapper { .wrapper {
width: 1000px; width: 80%;
height: $performance-bar-height;
margin: 0 auto; margin: 0 auto;
} }
......
...@@ -4,6 +4,7 @@ module NotesActions ...@@ -4,6 +4,7 @@ module NotesActions
included do included do
before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end end
def index def index
...@@ -28,7 +29,8 @@ module NotesActions ...@@ -28,7 +29,8 @@ module NotesActions
merge_request_diff_head_sha: params[:merge_request_diff_head_sha], merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id] in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
) )
@note = Notes::CreateService.new(project, current_user, create_params).execute
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note) if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user) Banzai::NoteRenderer.render([@note], @project, current_user)
...@@ -177,4 +179,22 @@ module NotesActions ...@@ -177,4 +179,22 @@ module NotesActions
def notes_finder def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params) @notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end end
def note_project
return @note_project if defined?(@note_project)
return nil unless project
note_project_id = params[:note_project_id]
@note_project =
if note_project_id.present?
Project.find(note_project_id)
else
project
end
return access_denied! unless can?(current_user, :create_note, @note_project)
@note_project
end
end end
...@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged] before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_recently_updated
@branches = BranchesFinder.new(@repository, params).execute @branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page]) @branches = Kaminari.paginate_array(@branches).page(params[:page])
......
...@@ -81,7 +81,6 @@ class IssuableFinder ...@@ -81,7 +81,6 @@ class IssuableFinder
end end
counts[:all] = counts.values.sum counts[:all] = counts.values.sum
counts[:opened] += counts[:reopened]
counts counts
end end
......
...@@ -264,7 +264,11 @@ module ApplicationHelper ...@@ -264,7 +264,11 @@ module ApplicationHelper
end end
def page_class def page_class
"issue-boards-page" if current_controller?(:boards) class_names = []
class_names << 'issue-boards-page' if current_controller?(:boards)
class_names << 'with-performance-bar' if performance_bar_enabled?
class_names
end end
# Returns active css class when condition returns true # Returns active css class when condition returns true
......
...@@ -15,7 +15,7 @@ module DiffHelper ...@@ -15,7 +15,7 @@ module DiffHelper
def diff_view def diff_view
@diff_view ||= begin @diff_view ||= begin
diff_views = %w(inline parallel) diff_views = %w(inline parallel)
diff_view = cookies[:diff_view] diff_view = params[:view] || cookies[:diff_view]
diff_view = diff_views.first unless diff_views.include?(diff_view) diff_view = diff_views.first unless diff_views.include?(diff_view)
diff_view.to_sym diff_view.to_sym
end end
......
module NavHelper module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar
class_name
end
def page_gutter_class def page_gutter_class
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') || current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') || current_path?('issues#show') ||
current_path?('milestones#show') current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed" %w[page-gutter right-sidebar-collapsed]
else else
"page-gutter right-sidebar-expanded" %w[page-gutter right-sidebar-expanded]
end end
elsif current_path?('jobs#show') elsif current_path?('jobs#show')
"page-gutter build-sidebar right-sidebar-expanded" %w[page-gutter build-sidebar right-sidebar-expanded]
elsif current_path?('wikis#show') || elsif current_path?('wikis#show') ||
current_path?('wikis#edit') || current_path?('wikis#edit') ||
current_path?('wikis#update') || current_path?('wikis#update') ||
current_path?('wikis#history') || current_path?('wikis#history') ||
current_path?('wikis#git_access') current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded" %w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
end end
end end
def nav_header_class def nav_header_class
class_name = '' class_names = []
class_name << " with-horizontal-nav" if defined?(nav) && nav class_names << 'with-horizontal-nav' if defined?(nav) && nav
class_name class_names
end end
def layout_nav_class def layout_nav_class
class_name = '' return [] if show_new_nav?
class_name << " page-with-layout-nav" if defined?(nav) && nav
class_name << " page-with-sub-nav" if content_for?(:sub_nav)
class_name class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav
class_names << 'page-with-sub-nav' if content_for?(:sub_nav)
class_names
end end
def nav_control_class def nav_control_class
......
...@@ -62,7 +62,11 @@ module NotesHelper ...@@ -62,7 +62,11 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
data = { discussion_id: discussion.reply_id, line_type: line_type } data = {
discussion_id: discussion.reply_id,
discussion_project_id: discussion.project&.id,
line_type: line_type
}
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply' data: data, title: 'Add a reply'
......
...@@ -34,6 +34,8 @@ module WebpackHelper ...@@ -34,6 +34,8 @@ module WebpackHelper
end end
def webpack_public_path def webpack_public_path
"#{webpack_public_host}/#{Rails.application.config.webpack.public_path}/" relative_path = Rails.application.config.relative_url_root
webpack_path = Rails.application.config.webpack.public_path
File.join(webpack_public_host.to_s, relative_path.to_s, webpack_path.to_s, '')
end end
end end
...@@ -71,9 +71,8 @@ module Issuable ...@@ -71,9 +71,8 @@ module Issuable
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :opened, -> { with_state(:opened, :reopened) } scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :only_reopened, -> { with_state(:reopened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
...@@ -234,7 +233,7 @@ module Issuable ...@@ -234,7 +233,7 @@ module Issuable
end end
def open? def open?
opened? || reopened? opened?
end end
def user_notes_count def user_notes_count
......
...@@ -62,15 +62,14 @@ class Issue < ActiveRecord::Base ...@@ -62,15 +62,14 @@ class Issue < ActiveRecord::Base
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:opened] => :closed
end end
event :reopen do event :reopen do
transition closed: :reopened transition closed: :opened
end end
state :opened state :opened
state :reopened
state :closed state :closed
before_transition any => :closed do |issue| before_transition any => :closed do |issue|
......
...@@ -42,23 +42,23 @@ class MergeRequest < ActiveRecord::Base ...@@ -42,23 +42,23 @@ class MergeRequest < ActiveRecord::Base
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:opened] => :closed
end end
event :mark_as_merged do event :mark_as_merged do
transition [:reopened, :opened, :locked] => :merged transition [:opened, :locked] => :merged
end end
event :reopen do event :reopen do
transition closed: :reopened transition closed: :opened
end end
event :lock_mr do event :lock_mr do
transition [:reopened, :opened] => :locked transition [:opened] => :locked
end end
event :unlock_mr do event :unlock_mr do
transition locked: :reopened transition locked: :opened
end end
after_transition any => :locked do |merge_request, transition| after_transition any => :locked do |merge_request, transition|
...@@ -72,7 +72,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -72,7 +72,6 @@ class MergeRequest < ActiveRecord::Base
end end
state :opened state :opened
state :reopened
state :closed state :closed
state :merged state :merged
state :locked state :locked
...@@ -368,7 +367,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -368,7 +367,7 @@ class MergeRequest < ActiveRecord::Base
errors.add :branch_conflict, "You can not use same project/branch for source and target" errors.add :branch_conflict, "You can not use same project/branch for source and target"
end end
if opened? || reopened? if opened?
similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any? if similar_mrs.any?
......
...@@ -114,7 +114,7 @@ class DroneCiService < CiService ...@@ -114,7 +114,7 @@ class DroneCiService < CiService
end end
def merge_request_valid?(data) def merge_request_valid?(data)
%w(opened reopened).include?(data[:object_attributes][:state]) && data[:object_attributes][:state] == 'opened' &&
data[:object_attributes][:merge_status] == 'unchecked' data[:object_attributes][:merge_status] == 'unchecked'
end end
end end
...@@ -45,6 +45,7 @@ class GitPushService < BaseService ...@@ -45,6 +45,7 @@ class GitPushService < BaseService
elsif push_to_existing_branch? elsif push_to_existing_branch?
# Collect data for this git push # Collect data for this git push
@push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev])
process_commit_messages process_commit_messages
# Update the bare repositories info/attributes file using the contents of the default branches # Update the bare repositories info/attributes file using the contents of the default branches
...@@ -66,15 +67,21 @@ class GitPushService < BaseService ...@@ -66,15 +67,21 @@ class GitPushService < BaseService
def update_caches def update_caches
if is_default_branch? if is_default_branch?
if push_to_new_branch?
# If this is the initial push into the default branch, the file type caches
# will already be reset as a result of `Project#change_head`.
types = []
else
paths = Set.new paths = Set.new
@push_commits.each do |commit| @push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
commit.raw_deltas.each do |diff| commit.raw_deltas.each do |diff|
paths << diff.new_path paths << diff.new_path
end end
end end
types = Gitlab::FileDetector.types_in_paths(paths.to_a) types = Gitlab::FileDetector.types_in_paths(paths.to_a)
end
else else
types = [] types = []
end end
...@@ -92,7 +99,7 @@ class GitPushService < BaseService ...@@ -92,7 +99,7 @@ class GitPushService < BaseService
def process_commit_messages def process_commit_messages
default = is_default_branch? default = is_default_branch?
push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit| @push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
if commit.matches_cross_reference_regex? if commit.matches_cross_reference_regex?
ProcessCommitWorker ProcessCommitWorker
.perform_async(project.id, current_user.id, commit.to_hash, default) .perform_async(project.id, current_user.id, commit.to_hash, default)
...@@ -131,7 +138,10 @@ class GitPushService < BaseService ...@@ -131,7 +138,10 @@ class GitPushService < BaseService
end end
def process_default_branch def process_default_branch
@push_commits = project.repository.commits(params[:newrev]) @push_commits_count = project.repository.commit_count_for_ref(params[:ref])
offset = [@push_commits_count - PROCESS_COMMIT_LIMIT, 0].max
@push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
# Ensure HEAD points to the default branch in case it is not master # Ensure HEAD points to the default branch in case it is not master
project.change_head(branch_name) project.change_head(branch_name)
...@@ -160,7 +170,8 @@ class GitPushService < BaseService ...@@ -160,7 +170,8 @@ class GitPushService < BaseService
params[:oldrev], params[:oldrev],
params[:newrev], params[:newrev],
params[:ref], params[:ref],
push_commits) @push_commits,
commits_count: @push_commits_count)
end end
def push_to_existing_branch? def push_to_existing_branch?
......
...@@ -5,7 +5,7 @@ module Issues ...@@ -5,7 +5,7 @@ module Issues
if issue.reopen if issue.reopen
event_service.reopen_issue(issue, current_user) event_service.reopen_issue(issue, current_user)
create_note(issue) create_note(issue, 'reopened')
notification_service.reopen_issue(issue, current_user) notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen') execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees) invalidate_cache_counts(issue, users: issue.assignees)
...@@ -16,8 +16,8 @@ module Issues ...@@ -16,8 +16,8 @@ module Issues
private private
def create_note(issue) def create_note(issue, state = issue.state)
SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil) SystemNoteService.change_status(issue, issue.project, current_user, state, nil)
end end
end end
end end
module MergeRequests module MergeRequests
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
def create_note(merge_request) def create_note(merge_request, state = merge_request.state)
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end end
def create_title_change_note(issuable, old_title) def create_title_change_note(issuable, old_title)
...@@ -44,7 +44,7 @@ module MergeRequests ...@@ -44,7 +44,7 @@ module MergeRequests
end end
# Returns all origin and fork merge requests from `@project` satisfying passed arguments. # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened, :reopened]) def merge_requests_for(source_branch, mr_states: [:opened])
MergeRequest MergeRequest
.with_state(mr_states) .with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id) .where(source_branch: source_branch, source_project_id: @project.id)
......
...@@ -5,7 +5,7 @@ module MergeRequests ...@@ -5,7 +5,7 @@ module MergeRequests
if merge_request.reopen if merge_request.reopen
event_service.reopen_mr(merge_request, current_user) event_service.reopen_mr(merge_request, current_user)
create_note(merge_request) create_note(merge_request, 'reopened')
notification_service.reopen_mr(merge_request, current_user) notification_service.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen') execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user) merge_request.reload_diff(current_user)
......
...@@ -4,6 +4,9 @@ module QuickActions ...@@ -4,6 +4,9 @@ module QuickActions
attr_reader :issuable attr_reader :issuable
SHRUG = '¯\\_(ツ)_/¯'.freeze
TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze
# Takes a text and interprets the commands that are extracted from it. # Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record. # Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable) def execute(content, issuable)
...@@ -14,6 +17,7 @@ module QuickActions ...@@ -14,6 +17,7 @@ module QuickActions
content, commands = extractor.extract_commands(content, context) content, commands = extractor.extract_commands(content, context)
extract_updates(commands, context) extract_updates(commands, context)
[content, @updates] [content, @updates]
end end
...@@ -423,6 +427,18 @@ module QuickActions ...@@ -423,6 +427,18 @@ module QuickActions
@updates[:spend_time] = { duration: :reset, user: current_user } @updates[:spend_time] = { duration: :reset, user: current_user }
end end
desc "Append the comment with #{SHRUG}"
params '<Comment>'
substitution :shrug do |comment|
"#{comment} #{SHRUG}"
end
desc "Append the comment with #{TABLEFLIP}"
params '<Comment>'
substitution :tableflip do |comment|
"#{comment} #{TABLEFLIP}"
end
# This is a dummy command, so that it appears in the autocomplete commands # This is a dummy command, so that it appears in the autocomplete commands
desc 'CC' desc 'CC'
params '@user' params '@user'
......
...@@ -44,7 +44,7 @@ class WebHookService ...@@ -44,7 +44,7 @@ class WebHookService
http_status: response.code, http_status: response.code,
message: response.to_s message: response.to_s
} }
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e
log_execution( log_execution(
trigger: hook_name, trigger: hook_name,
url: hook.url, url: hook.url,
......
.page-with-sidebar{ class: "#{('page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar)} #{page_gutter_class}" } .page-with-sidebar{ class: page_with_sidebar_class }
- if show_new_nav? - if show_new_nav?
- if defined?(nav) && nav - if defined?(nav) && nav
= render "layouts/nav/#{nav}" = render "layouts/nav/#{nav}"
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= render "layouts/nav/#{nav}" = render "layouts/nav/#{nav}"
- if content_for?(:sub_nav) - if content_for?(:sub_nav)
= yield :sub_nav = yield :sub_nav
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" } .content-wrapper{ class: layout_nav_class }
- if show_new_nav? - if show_new_nav?
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
......
!!! 5 !!! 5
%html{ lang: I18n.locale, class: "#{page_class}" } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- if show_new_nav? - if show_new_nav?
= render "layouts/header/new" = render "layouts/header/new"
- else - else
...@@ -10,5 +11,3 @@ ...@@ -10,5 +11,3 @@
= render 'layouts/page', sidebar: sidebar, nav: nav = render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body = yield :scripts_body
= render 'peek/bar'
...@@ -91,8 +91,8 @@ ...@@ -91,8 +91,8 @@
= nav_link(controller: :abuse_reports) do = nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do = link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span %span
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all)) %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
Abuse Reports
- if akismet_enabled? - if akismet_enabled?
= nav_link(controller: :spam_logs) do = nav_link(controller: :spam_logs) do
......
...@@ -28,9 +28,9 @@ ...@@ -28,9 +28,9 @@
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do = link_to issues_group_path(@group), title: 'Issues' do
%span %span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count) %span.badge.count= number_with_delimiter(issues.count)
Issues
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
...@@ -51,9 +51,9 @@ ...@@ -51,9 +51,9 @@
= nav_link(path: 'groups#merge_requests') do = nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span %span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count) %span.badge.count= number_with_delimiter(merge_requests.count)
Merge Requests
= nav_link(path: 'group_members#index') do = nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do = link_to group_group_members_path(@group), title: 'Members' do
%span %span
......
%span.current-host
= truncate(view.hostname)
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
- page_title 'New Project' - page_title 'New Project'
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'project_new'
.project-edit-container .project-edit-container
.project-edit-errors .project-edit-errors
...@@ -111,46 +113,3 @@ ...@@ -111,46 +113,3 @@
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
Creating project &amp; repository. Creating project &amp; repository.
%p Please wait a moment, this page will automatically refresh when ready. %p Please wait a moment, this page will automatically refresh when ready.
:javascript
var importBtnTooltip = "Please enter a valid project name.";
var $importBtnWrapper = $('.import_gitlab_project');
$('.how_to_import_link').bind('click', function (e) {
e.preventDefault();
var import_modal = $(this).next(".modal").show();
});
$('.modal-header .close').bind('click', function() {
$(".modal").hide();
});
$('.btn_import_gitlab_project').bind('click', function() {
var _href = $("a.btn_import_gitlab_project").attr("href");
$(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
$('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
$importBtnWrapper.attr('title', importBtnTooltip);
$('#new_project').submit(function(){
var $path = $('#project_path');
$path.val($path.val().trim());
});
$('#project_path').keyup(function(){
if($(this).val().trim().length !== 0) {
$('.btn_import_gitlab_project').attr('disabled', false);
$importBtnWrapper.attr('title','');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled',true);
$importBtnWrapper.addClass('has-tooltip');
}
});
$('#project_import_url').disable();
$('.import_git').click(function( event ) {
$projectImportUrl = $('#project_import_url');
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
});
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_times')
%div %div
%p.light %p.light
= _("Commit duration in minutes for last 30 commits") = _("Commit duration in minutes for last 30 commits")
%canvas#build_timesChart{ height: 200 } %canvas#build_timesChart{ height: 200 }
:javascript %script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
var data = {
labels : #{@charts[:pipeline_times].labels.to_json},
datasets : [
{
fillColor : "rgba(220,220,220,0.5)",
strokeColor : "rgba(220,220,220,1)",
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data : #{@charts[:pipeline_times].pipeline_times.to_json}
}
]
}
var ctx = $("#build_timesChart").get(0).getContext("2d");
var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8
}
new Chart(ctx).Bar(data, options);
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_charts')
%h4= _("Pipelines charts") %h4= _("Pipelines charts")
%p %p
&nbsp; &nbsp;
...@@ -26,31 +29,8 @@ ...@@ -26,31 +29,8 @@
= _("Jobs for last year") = _("Jobs for last year")
%canvas#yearChart.padded{ height: 250 } %canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope| %script#pipelinesChartsData{ type: "application/json" }
:javascript - chartData = []
var data = { - [:week, :month, :year].each do |scope|
labels : #{@charts[scope].labels.to_json}, - chartData.push({ 'scope' => scope, 'labels' => @charts[scope].labels, 'totalValues' => @charts[scope].total, 'successValues' => @charts[scope].success })
datasets : [ = chartData.to_json.html_safe
{
fillColor : "#7f8fa4",
strokeColor : "#7f8fa4",
pointColor : "#7f8fa4",
pointStrokeColor : "#EEE",
data : #{@charts[scope].total.to_json}
},
{
fillColor : "#44aa22",
strokeColor : "#44aa22",
pointColor : "#44aa22",
pointStrokeColor : "#fff",
data : #{@charts[scope].success.to_json}
}
]
}
var ctx = $("##{scope}Chart").get(0).getContext("2d");
var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8
}
new Chart(ctx).Line(data, options);
...@@ -20,7 +20,4 @@ ...@@ -20,7 +20,4 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
:javascript %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
var availableRefs = #{@project.repository.ref_names.to_json};
new NewBranchForm($('.js-new-pipeline-form'), availableRefs)
...@@ -40,7 +40,4 @@ ...@@ -40,7 +40,4 @@
.form-actions .form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
:javascript
window.gl = window.gl || { };
window.gl.availableRefs = #{@project.repository.ref_names.to_json};
.tree-content-holder .tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
.table-holder .table-holder
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
%thead %thead
......
...@@ -19,6 +19,3 @@ ...@@ -19,6 +19,3 @@
More Pages More Pages
= render 'projects/wikis/new' = render 'projects/wikis/new'
:javascript
new Sidebar();
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
= hidden_field_tag :line_type = hidden_field_tag :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= hidden_field_tag :in_reply_to_discussion_id = hidden_field_tag :in_reply_to_discussion_id
= hidden_field_tag :note_project_id
= note_target_fields(@note) = note_target_fields(@note)
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- if avatar - if avatar
.avatar-container.s40 .avatar-container.s40
= link_to project_path(project), class: dom_class(project) do = link_to project_path(project), class: dom_class(project) do
- if use_creator_avatar - if project.creator && use_creator_avatar
= image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- else - else
= project_icon(project, alt: '', class: 'avatar project-avatar s40') = project_icon(project, alt: '', class: 'avatar project-avatar s40')
......
...@@ -5,6 +5,12 @@ class GitGarbageCollectWorker ...@@ -5,6 +5,12 @@ class GitGarbageCollectWorker
sidekiq_options retry: false sidekiq_options retry: false
GITALY_MIGRATED_TASKS = {
gc: :garbage_collect,
full_repack: :repack_full,
incremental_repack: :repack_incremental
}.freeze
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id) project = Project.find(project_id)
task = task.to_sym task = task.to_sym
...@@ -15,8 +21,14 @@ class GitGarbageCollectWorker ...@@ -15,8 +21,14 @@ class GitGarbageCollectWorker
Gitlab::GitLogger.info(description) Gitlab::GitLogger.info(description)
gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled|
if is_enabled
gitaly_call(task, project.repository.raw_repository)
else
output, status = Gitlab::Popen.popen(cmd, repo_path) output, status = Gitlab::Popen.popen(cmd, repo_path)
Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero? Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero?
end
end
# Refresh the branch cache in case garbage collection caused a ref lookup to fail # Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc flush_ref_caches(project) if task == :gc
...@@ -26,6 +38,19 @@ class GitGarbageCollectWorker ...@@ -26,6 +38,19 @@ class GitGarbageCollectWorker
private private
## `repository` has to be a Gitlab::Git::Repository
def gitaly_call(task, repository)
client = Gitlab::GitalyClient::RepositoryService.new(repository)
case task
when :gc
client.garbage_collect(bitmaps_enabled?)
when :full_repack
client.repack_full(bitmaps_enabled?)
when :incremental_repack
client.repack_incremental
end
end
def command(task) def command(task)
case task case task
when :gc when :gc
...@@ -55,4 +80,14 @@ class GitGarbageCollectWorker ...@@ -55,4 +80,14 @@ class GitGarbageCollectWorker
config_value = write_bitmaps ? 'true' : 'false' config_value = write_bitmaps ? 'true' : 'false'
%W[git -c repack.writeBitmaps=#{config_value}] %W[git -c repack.writeBitmaps=#{config_value}]
end end
def gitaly_migrate(method, &block)
Gitlab::GitalyClient.migrate(method, &block)
rescue GRPC::NotFound => e
Gitlab::GitLogger.error("#{method} failed:\nRepository not found")
raise Gitlab::Git::Repository::NoRepository.new(e)
rescue GRPC::BadStatus => e
Gitlab::GitLogger.error("#{method} failed:\n#{e}")
raise Gitlab::Git::CommandError.new(e)
end
end end
---
title: "Fix v3 api project_hooks POST and PUT operations for build_events"
merge_request: 12673
author: Richard Clamp
---
title: Add /shrug and /tableflip commands
merge_request: 10068
author: Alex Ives
---
title: Fix project logos that are not centered vertically on list pages
merge_request: 13124
author: Florian Lemaitre
---
title: Fix asynchronous javascript paths when GitLab is installed under a relative
URL
merge_request: 13165
author:
---
title: Properly affixes nav bar in job view in microsoft edge
merge_request:
author:
---
title: Fix display of new diff comments after changing b between diff views
merge_request:
author:
---
title: Fixed breadcrumbs title aggressively collapsing
merge_request:
author:
---
title: Improve performance of large (initial) push into default branch
merge_request:
author:
---
title: Fix LDAP authentication to Git repository or container registry
merge_request:
author:
---
title: Modify if condition to be more readable
merge_request:
author:
---
title: Improve deploy environment chatops slash command
merge_request: 13150
author:
---
title: Fix replying to commit comments on merge requests created from forks
merge_request:
author:
---
title: Merge issuable "reopened" state into "opened"
merge_request:
author:
...@@ -223,7 +223,7 @@ Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_c ...@@ -223,7 +223,7 @@ Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_c
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80)
Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || ''
Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
......
...@@ -55,8 +55,11 @@ var config = { ...@@ -55,8 +55,11 @@ var config = {
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js', pipelines: './pipelines/pipelines_bundle.js',
pipelines_charts: './pipelines/pipelines_charts.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
project_new: './projects/project_new.js',
prometheus_metrics: './prometheus_metrics', prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class MergeIssuableReopenedIntoOpenedState < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
end
class MergeRequest < ActiveRecord::Base
self.table_name = 'merge_requests'
include EachBatch
end
def up
[Issue, MergeRequest].each do |model|
say "Changing #{model.table_name}.state from 'reopened' to 'opened'"
model.where(state: 'reopened').each_batch do |batch|
batch.update_all(state: 'opened')
end
end
end
end
...@@ -254,32 +254,32 @@ ActiveRecord::Schema.define(version: 20170725145659) do ...@@ -254,32 +254,32 @@ ActiveRecord::Schema.define(version: 20170725145659) do
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
create_table "ci_pipeline_schedule_variables", force: :cascade do |t| create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
t.text "encrypted_value" t.text "encrypted_value"
t.string "encrypted_value_salt" t.string "encrypted_value_salt"
t.string "encrypted_value_iv" t.string "encrypted_value_iv"
t.integer "pipeline_schedule_id", null: false t.integer "group_id", null: false
t.boolean "protected", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
create_table "ci_group_variables", force: :cascade do |t| create_table "ci_pipeline_schedule_variables", force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
t.text "encrypted_value" t.text "encrypted_value"
t.string "encrypted_value_salt" t.string "encrypted_value_salt"
t.string "encrypted_value_iv" t.string "encrypted_value_iv"
t.integer "group_id", null: false t.integer "pipeline_schedule_id", null: false
t.boolean "protected", default: false, null: false t.datetime "created_at"
t.datetime "created_at", null: false t.datetime "updated_at"
t.datetime "updated_at", null: false
end end
add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
create_table "ci_pipeline_schedules", force: :cascade do |t| create_table "ci_pipeline_schedules", force: :cascade do |t|
t.string "description" t.string "description"
...@@ -1624,8 +1624,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do ...@@ -1624,8 +1624,8 @@ ActiveRecord::Schema.define(version: 20170725145659) do
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade
add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
......
...@@ -44,16 +44,17 @@ Shortcuts to GitLab's most visited docs: ...@@ -44,16 +44,17 @@ Shortcuts to GitLab's most visited docs:
### Projects and groups ### Projects and groups
- [Create a project](gitlab-basics/create-project.md) - [Projects](user/project/index.md):
- [Fork a project](gitlab-basics/fork-project.md) - [Create a project](gitlab-basics/create-project.md)
- [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Fork a project](gitlab-basics/fork-project.md)
- [Project access](public_access/public_access.md): Setting up your project's visibility to public, internal, or private. - [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Project access](public_access/public_access.md): Setting up your project's visibility to public, internal, or private.
- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages.
- [Groups](user/group/index.md): Organize your projects in groups. - [Groups](user/group/index.md): Organize your projects in groups.
- [GitLab Subgroups](user/group/subgroups/index.md) - [Subgroups](user/group/subgroups/index.md)
- [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards. - [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards.
- [Snippets](user/snippets.md): Snippets allow you to create little bits of code. - [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis. - [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis.
- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages.
### Repository ### Repository
......
...@@ -46,6 +46,10 @@ GitLab does not recommend using EFS with GitLab. ...@@ -46,6 +46,10 @@ GitLab does not recommend using EFS with GitLab.
many small files are written in a serialized manner are not well-suited for EFS. many small files are written in a serialized manner are not well-suited for EFS.
EBS with an NFS server on top will perform much better. EBS with an NFS server on top will perform much better.
In addition, avoid storing GitLab log files (e.g. those in `/var/log/gitlab`)
because this will also affect performance. We recommend that the log files be
stored on a local volume.
For more details on another person's experience with EFS, see For more details on another person's experience with EFS, see
[Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/) [Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/)
......
...@@ -258,7 +258,7 @@ Parameters: ...@@ -258,7 +258,7 @@ Parameters:
- `location` (optional) - User's location - `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false - `skip_confirmation` (optional) - Skip confirmation - true or false (default)
- `external` (optional) - Flags the user as external - true or false(default) - `external` (optional) - Flags the user as external - true or false(default)
- `avatar` (optional) - Image file for user's avatar - `avatar` (optional) - Image file for user's avatar
......
...@@ -441,13 +441,25 @@ There are a few rules that apply to the usage of refs policy: ...@@ -441,13 +441,25 @@ There are a few rules that apply to the usage of refs policy:
* `only` and `except` are inclusive. If both `only` and `except` are defined * `only` and `except` are inclusive. If both `only` and `except` are defined
in a job specification, the ref is filtered by `only` and `except`. in a job specification, the ref is filtered by `only` and `except`.
* `only` and `except` allow the use of regular expressions. * `only` and `except` allow the use of regular expressions.
* `only` and `except` allow the use of special keywords:
`api`, `branches`, `external`, `tags`, `pushes`, `schedules`, `triggers`, and `web`
* `only` and `except` allow to specify a repository path to filter jobs for * `only` and `except` allow to specify a repository path to filter jobs for
forks. forks.
In addition, `only` and `except` allow the use of special keywords:
| **Value** | **Description** |
| --------- | ---------------- |
| `branches` | When a branch is pushed. |
| `tags` | When a tag is pushed. |
| `api` | When pipeline has been triggered by a second pipelines API (not triggers API). |
| `external` | When using CI services other than GitLab. |
| `pipelines` | For multi-project triggers, created using the API with `CI_JOB_TOKEN`. |
| `pushes` | Pipeline is triggered by a `git push` by the user. |
| `schedules` | For [scheduled pipelines][schedules]. |
| `triggers` | For pipelines created using a trigger token. |
| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
In the example below, `job` will run only for refs that start with `issue-`, In the example below, `job` will run only for refs that start with `issue-`,
whereas all branches will be skipped. whereas all branches will be skipped:
```yaml ```yaml
job: job:
...@@ -460,7 +472,7 @@ job: ...@@ -460,7 +472,7 @@ job:
``` ```
In this example, `job` will run only for refs that are tagged, or if a build is In this example, `job` will run only for refs that are tagged, or if a build is
explicitly requested via an API trigger or a [Pipeline Schedule](../../user/project/pipelines/schedules.md). explicitly requested via an API trigger or a [Pipeline Schedule][schedules]:
```yaml ```yaml
job: job:
...@@ -1532,3 +1544,4 @@ CI with various languages. ...@@ -1532,3 +1544,4 @@ CI with various languages.
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 [ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447 [ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
[ce-3442]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3442 [ce-3442]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3442
[schedules]: ../../user/project/pipelines/schedules.md
...@@ -106,6 +106,14 @@ When using verbs or adjectives: ...@@ -106,6 +106,14 @@ When using verbs or adjectives:
* If the context clearly refers to the object, use them alone. Example: `Edit` or `Closed` * If the context clearly refers to the object, use them alone. Example: `Edit` or `Closed`
* If the context isn’t clear enough, use them with the object. Example: `Edit issue` or `Closed issues` * If the context isn’t clear enough, use them with the object. Example: `Edit issue` or `Closed issues`
### Search
| Term | Use |
| ---- | --- |
| Search | When using all metadata to add criteria that match/don't match. Search can also affect ordering, by ranking best results. |
| Filter | When taking a single criteria that removes items within a list that match/don't match. Filters do not affect ordering. |
| Sort | Orders a list based on a single or grouped criteria |
### Projects and Groups ### Projects and Groups
| Term | Use | :no_entry_sign: Don't | | Term | Use | :no_entry_sign: Don't |
......
...@@ -2,7 +2,11 @@ ...@@ -2,7 +2,11 @@
Slash commands in Mattermost and Slack allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it. Slash commands in Mattermost and Slack allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it.
Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are: Commands are scoped to a project, with a trigger term that is specified during configuration.
We suggest you use the project name as the trigger term for simplicity and clarity.
Taking the trigger term as `project-name`, the commands are:
| Command | Effect | | Command | Effect |
...@@ -12,3 +16,18 @@ Commands are scoped to a project, with a trigger term that is specified during c ...@@ -12,3 +16,18 @@ Commands are scoped to a project, with a trigger term that is specified during c
| `/project-name issue show <id>` | Shows the issue with id `<id>` | | `/project-name issue show <id>` | Shows the issue with id `<id>` |
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` | | `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment | | `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
## Issue commands
It is possible to create new issue, display issue details and search up to 5 issues.
## Deploy command
In order to deploy to an environment, GitLab will try to find a deployment
manual action in the pipeline.
If there is only one action for a given environment, it is going to be triggered.
If there is more than one action defined, GitLab will try to find an action
which name equals the environment name we want to deploy to.
Command will return an error when no matching action has been found.
...@@ -122,6 +122,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project ...@@ -122,6 +122,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) 1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw) 1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc) 1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
2. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times)
1. See **[Integrations](#integrations)** for integrations with other CI services. 1. See **[Integrations](#integrations)** for integrations with other CI services.
#### 2.4. Workflow #### 2.4. Workflow
......
...@@ -66,7 +66,7 @@ For more use cases please check our [Technical Articles](../articles/index.md). ...@@ -66,7 +66,7 @@ For more use cases please check our [Technical Articles](../articles/index.md).
## Projects ## Projects
In GitLab, you can create projects for numerous reasons, such as, host In GitLab, you can create [projects](project/index.md) for numerous reasons, such as, host
your code, use it as an issue tracker, collaborate on code, and continuously your code, use it as an issue tracker, collaborate on code, and continuously
build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do
it all at once, from one single project. it all at once, from one single project.
......
...@@ -23,7 +23,7 @@ On your profile page, you will see the following information: ...@@ -23,7 +23,7 @@ On your profile page, you will see the following information:
- Personal information - Personal information
- Activity stream: see your activity streamline and the history of your contributions - Activity stream: see your activity streamline and the history of your contributions
- Groups: [groups](../group/index.md) you're a member of - Groups: [groups](../group/index.md) you're a member of
- Contributed projects: projects you contributed to - Contributed projects: [projects](../project/index.md) you contributed to
- Personal projects: your personal projects (respecting the project's visibility level) - Personal projects: your personal projects (respecting the project's visibility level)
- Snippets: your personal code [snippets](../snippets.md#personal-snippets) - Snippets: your personal code [snippets](../snippets.md#personal-snippets)
......
# Projects
In GitLab, you can create projects for hosting
your codebase, use it as an issue tracker, collaborate on code, and continuously
build, test, and deploy your app with built-in GitLab CI/CD.
Your projects can be [available](../../public_access/public_access.md)
publicly, internally, or privately, at your choice. GitLab does not limit
the number of private projects you create.
## Project's features
When you create a project in GitLab, you'll have access to a large number of
[features](https://about.gitlab.com/features/):
**Issues and merge requests:**
- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
- [Issue Boards](issue_board.md): Organize and prioritize your workflow
- [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards) (**EES/EEP**): Allow your teams to create their own workflows (Issue Boards) for the same project
- [Repositories](repository/index.md): Host your code in a fully
integrated platform
- [Protected branches](protected_branches.md): Prevent collaborators
from messing with history or pushing code without review
- [Protected tags](protected_tags.md): Control over who has
permission to create tags, and prevent accidental update or deletion
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**EES/EEP**): Ask for approval before
implementing a change
- [Fix merge conflicts from the UI](merge_requests/resolve_conflicts.md):
Your Git diff tool right from GitLab's UI
- [Review Apps](../../ci/review_apps/index.md): Live preview the results
of the changes proposed in a merge request in a per-branch basis
- [Labels](labels.md): Organize issues and merge requests by labels
- [Time Tracking](../../workflow/time_tracking.md): Track estimate time
and time spent on
the conclusion of an issue or merge request
- [Milestones](milestones/index.md): Work towards a target date
- [Description templates](description_templates.md): Define context-specific
templates for issue and merge request description fields for your project
- [Slash commands (quick actions)](quick_actions.md): Textual shortcuts for
common actions on issues or merge requests
**GitLab CI/CD:**
- [GitLab CI/CD](../../ci/README.md): GitLab's built-in [Continuous Integration, Delivery, and Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) tool
- [Container Registry](container_registry.md): Build and push Docker
images out-of-the-box
- [Auto Deploy](../../ci/autodeploy/index.md): Configure GitLab CI/CD
to automatically set up your app's deployment
- [Enable and disable GitLab CI](../../ci/enable_or_disable_ci.md)
- [Pipelines](../../ci/pipelines.md#pipelines): Configure and visualize
your GitLab CI/CD pipelines from the UI
- [Scheduled Pipelines](pipelines/schedules.md): Schedule a pipeline
to start at a chosen time
- [Pipeline Graphs](../../ci/pipelines.md#pipeline-graphs): View your
entire pipeline from the UI
- [Job artifacts](pipelines/job_artifacts.md): Define,
browse, and download job artifacts
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
**Other features:**
- [Cycle Analytics](cycle_analytics.md): Review your development lifecycle
- [Koding integration](koding.md) (not available on GitLab.com): Integrate
with Koding to have access to a web terminal right from the GitLab UI
- [Syntax highlighting](highlighting.md): An alternative to customize
your code blocks, overriding GitLab's default choice of language
### Project's integrations
[Integrate your project](integrations/index.md) with Jira, Mattermost,
Kubernetes, Slack, and a lot more.
## New project
Learn how to [create a new project](../../gitlab-basics/create-project.md) in GitLab.
### Fork a project
You can [fork a project](../../gitlab-basics/fork-project.md) in order to:
- Collaborate on code by forking a project and creating a merge request
from your fork to the upstream project
- Fork a sample project to work on the top of that
## Import or export a project
- Import a project from:
- [GitHub to GitLab](../../workflow/importing/import_projects_from_github.md)
- [BitBucket to GitLab](../../workflow/importing/import_projects_from_bitbucket.md)
- [Gitea to GitLab](../../workflow/importing/import_projects_from_gitea.md)
- [FogBugz to GitLab](../../workflow/importing/import_projects_from_fogbugz.md)
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
## Leave a project
**Leave project** will only display on the project's dashboard
when a project is part of a group (under a
[group namespace](../group/index.md#namespaces)).
If you choose to leave a project you will no longer be a project
member, therefore, unable to contribute.
# GitLab JIRA integration # GitLab JIRA integration
GitLab can be configured to interact with JIRA. Configuration happens via GitLab can be configured to interact with [JIRA], a project management platform.
user name and password. Connecting to a JIRA server via CAS is not possible.
Each project can be configured to connect to a different JIRA instance, see the Once your GitLab project is connected to JIRA, you can reference and close the
[configuration](#configuration) section. If you have one JIRA instance you can issues in JIRA directly from GitLab.
pre-fill the settings page with a default template. To configure the template
see the [Services Templates][services-templates] document.
Once the project is connected to JIRA, you can reference and close the issues For a use case, check out this article of [How and why to integrate GitLab with
in JIRA directly from GitLab. JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/how-to/2017/04/25).
## Configuration ## Configuration
Each GitLab project can be configured to connect to a different JIRA instance.
If you have one JIRA instance you can pre-fill the settings page with a default
template, see the [Services Templates][services-templates] docs.
Configuration happens via user name and password. Connecting to a JIRA server
via CAS is not possible.
In order to enable the JIRA service in GitLab, you need to first configure the In order to enable the JIRA service in GitLab, you need to first configure the
project in JIRA and then enter the correct values in GitLab. project in JIRA and then enter the correct values in GitLab.
...@@ -213,3 +217,4 @@ your project needs to close a ticket. ...@@ -213,3 +217,4 @@ your project needs to close a ticket.
[services-templates]: services_templates.md [services-templates]: services_templates.md
[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md [jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
[jira]: https://www.atlassian.com/software/jira
# Monitoring HA Proxy # Monitoring HAProxy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4
GitLab has support for automatically detecting and monitoring HA Proxy. This is provided by leveraging the [HA Proxy Exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates HA Proxy statistics into a Prometheus readable form. GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form.
## Metrics supported ## Metrics supported
...@@ -10,9 +10,9 @@ GitLab has support for automatically detecting and monitoring HA Proxy. This is ...@@ -10,9 +10,9 @@ GitLab has support for automatically detecting and monitoring HA Proxy. This is
| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) | | Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) |
| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) | | HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for HA Proxy metrics ## Configuring Prometheus to monitor for HAProxy metrics
To get started with NGINX monitoring, you should install and configure the [HA Proxy exporter](https://github.com/prometheus/haproxy_exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint. To get started with NGINX monitoring, you should install and configure the [HAProxy exporter](https://github.com/prometheus/haproxy_exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint.
## Specifying the Environment label ## Specifying the Environment label
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are: GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
* [Kubernetes](kubernetes.md) * [Kubernetes](kubernetes.md)
* [NGINX](nginx.md) * [NGINX](nginx.md)
* [HA Proxy](haproxy.md) * [HAProxy](haproxy.md)
* [Amazon Cloud Watch](cloudwatch.md) * [Amazon Cloud Watch](cloudwatch.md)
We have tried to surface the most important metrics for each exporter, and will be continuing to add support for additional exporters in future releases. If you would like to add support for other official exporters, [contributions](#adding-to-the-library) are welcome. We have tried to surface the most important metrics for each exporter, and will be continuing to add support for additional exporters in future releases. If you would like to add support for other official exporters, [contributions](#adding-to-the-library) are welcome.
......
# Koding & GitLab # Koding integration
> [Introduced][ce-5909] in GitLab 8.11. > [Introduced][ce-5909] in GitLab 8.11.
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
A [repository](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository) A [repository](https://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository)
is what you use to store your codebase in GitLab and change it with version control. is what you use to store your codebase in GitLab and change it with version control.
A repository is part of a project, which has a lot of other features. A repository is part of a [project](../index.md), which has a lot of other features.
## Create a repository ## Create a repository
......
...@@ -91,7 +91,6 @@ This workflow where commits only flow downstream ensures that everything has bee ...@@ -91,7 +91,6 @@ This workflow where commits only flow downstream ensures that everything has bee
If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch. If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches. If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches. If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
## Release branches with GitLab flow ## Release branches with GitLab flow
......
...@@ -12,7 +12,7 @@ module API ...@@ -12,7 +12,7 @@ module API
end end
def expose_url(path) def expose_url(path)
url_options = Rails.application.routes.default_url_options url_options = Gitlab::Application.routes.default_url_options
protocol, host, port = url_options.slice(:protocol, :host, :port).values protocol, host, port = url_options.slice(:protocol, :host, :port).values
URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s
......
...@@ -56,7 +56,9 @@ module API ...@@ -56,7 +56,9 @@ module API
use :project_hook_properties use :project_hook_properties
end end
post ":id/hooks" do post ":id/hooks" do
hook = user_project.hooks.new(declared_params(include_missing: false)) attrs = declared_params(include_missing: false)
attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
hook = user_project.hooks.new(attrs)
if hook.save if hook.save
present hook, with: ::API::V3::Entities::ProjectHook present hook, with: ::API::V3::Entities::ProjectHook
...@@ -77,7 +79,9 @@ module API ...@@ -77,7 +79,9 @@ module API
put ":id/hooks/:hook_id" do put ":id/hooks/:hook_id" do
hook = user_project.hooks.find(params.delete(:hook_id)) hook = user_project.hooks.find(params.delete(:hook_id))
if hook.update_attributes(declared_params(include_missing: false)) attrs = declared_params(include_missing: false)
attrs[:job_events] = attrs.delete(:build_events) if attrs.key?(:build_events)
if hook.update_attributes(attrs)
present hook, with: ::API::V3::Entities::ProjectHook present hook, with: ::API::V3::Entities::ProjectHook
else else
error!("Invalid url given", 422) if hook.errors[:url].present? error!("Invalid url given", 422) if hook.errors[:url].present?
......
...@@ -24,11 +24,11 @@ module Gitlab ...@@ -24,11 +24,11 @@ module Gitlab
# total_commits_count: Fixnum # total_commits_count: Fixnum
# } # }
# #
def build(project, user, oldrev, newrev, ref, commits = [], message = nil) def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil)
commits = Array(commits) commits = Array(commits)
# Total commits count # Total commits count
commits_count = commits.size commits_count ||= commits.size
# Get latest 20 commits ASC # Get latest 20 commits ASC
commits_limited = commits.last(20) commits_limited = commits.last(20)
......
...@@ -4,12 +4,28 @@ module Gitlab ...@@ -4,12 +4,28 @@ module Gitlab
def initialize(repository) def initialize(repository)
@repository = repository @repository = repository
@gitaly_repo = repository.gitaly_repository @gitaly_repo = repository.gitaly_repository
@storage = repository.storage
end end
def exists? def exists?
request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo) request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
GitalyClient.call(@repository.storage, :repository_service, :exists, request).exists GitalyClient.call(@storage, :repository_service, :exists, request).exists
end
def garbage_collect(create_bitmap)
request = Gitaly::GarbageCollectRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap)
GitalyClient.call(@storage, :repository_service, :garbage_collect, request)
end
def repack_full(create_bitmap)
request = Gitaly::RepackFullRequest.new(repository: @gitaly_repo, create_bitmap: create_bitmap)
GitalyClient.call(@storage, :repository_service, :repack_full, request)
end
def repack_incremental
request = Gitaly::RepackIncrementalRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :repack_incremental, request)
end end
end end
end end
......
...@@ -42,7 +42,7 @@ module Gitlab ...@@ -42,7 +42,7 @@ module Gitlab
end end
def adapter def adapter
OmniAuth::LDAP::Adaptor.new(config.options.symbolize_keys) OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
end end
def config def config
......
...@@ -105,9 +105,32 @@ module Gitlab ...@@ -105,9 +105,32 @@ module Gitlab
# # Awesome code block # # Awesome code block
# end # end
def command(*command_names, &block) def command(*command_names, &block)
define_command(CommandDefinition, *command_names, &block)
end
# Registers a new substitution which is recognizable from body of email or
# comment.
# It accepts aliases and takes a block with the formatted content.
#
# Example:
#
# command :my_substitution, :alias_for_my_substitution do |text|
# "#{text} MY AWESOME SUBSTITUTION"
# end
def substitution(*substitution_names, &block)
define_command(SubstitutionDefinition, *substitution_names, &block)
end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end
private
def define_command(klass, *command_names, &block)
name, *aliases = command_names name, *aliases = command_names
definition = CommandDefinition.new( definition = klass.new(
name, name,
aliases: aliases, aliases: aliases,
description: @description, description: @description,
...@@ -130,10 +153,6 @@ module Gitlab ...@@ -130,10 +153,6 @@ module Gitlab
@condition_block = nil @condition_block = nil
@parse_params_block = nil @parse_params_block = nil
end end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end
end end
end end
end end
......
...@@ -46,6 +46,8 @@ module Gitlab ...@@ -46,6 +46,8 @@ module Gitlab
end end
end end
content, commands = perform_substitutions(content, commands)
[content.strip, commands] [content.strip, commands]
end end
...@@ -110,6 +112,26 @@ module Gitlab ...@@ -110,6 +112,26 @@ module Gitlab
}mx }mx
end end
def perform_substitutions(content, commands)
return unless content
substitution_definitions = self.command_definitions.select do |definition|
definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
end
substitution_definitions.each do |substitution|
match_data = substitution.match(content)
if match_data
command = [substitution.name.to_s]
command << match_data[1] unless match_data[1].empty?
commands << command
end
content = substitution.perform_substitution(self, content)
end
[content, commands]
end
def command_names(opts) def command_names(opts)
command_definitions.flat_map do |command| command_definitions.flat_map do |command|
next if command.noop? next if command.noop?
......
module Gitlab
module QuickActions
class SubstitutionDefinition < CommandDefinition
# noop?=>true means these won't get extracted or removed by Gitlab::QuickActions::Extractor#extract_commands
# QuickActions::InterpretService#perform_substitutions handles them separately
def noop?
true
end
def match(content)
content.match %r{^/#{all_names.join('|')} ?(.*)$}
end
def perform_substitution(context, content)
return unless content
all_names.each do |a_name|
content.gsub!(%r{/#{a_name} ?(.*)$}, execute_block(action_block, context, '\1'))
end
content
end
end
end
end
...@@ -7,6 +7,14 @@ module Gitlab ...@@ -7,6 +7,14 @@ module Gitlab
class Controller < ActionController::Base class Controller < ActionController::Base
protect_from_forgery with: :exception protect_from_forgery with: :exception
rescue_from ActionController::InvalidAuthenticityToken do |e|
logger.warn "This CSRF token verification failure is handled internally by `GitLab::RequestForgeryProtection`"
logger.warn "Unlike the logs may suggest, this does not result in an actual 422 response to the user"
logger.warn "For API requests, the only effect is that `current_user` will be `nil` for the duration of the request"
raise e
end
def index def index
head :ok head :ok
end end
......
...@@ -21,29 +21,34 @@ module Gitlab ...@@ -21,29 +21,34 @@ module Gitlab
from = match[:from] from = match[:from]
to = match[:to] to = match[:to]
actions = find_actions(from, to) action = find_action(from, to)
if actions.none? if action.nil?
Gitlab::SlashCommands::Presenters::Deploy.new(nil).no_actions Gitlab::SlashCommands::Presenters::Deploy
elsif actions.one? .new(action).action_not_found
action = play!(from, to, actions.first)
Gitlab::SlashCommands::Presenters::Deploy.new(action).present(from, to)
else else
Gitlab::SlashCommands::Presenters::Deploy.new(actions).too_many_actions deployment = action.play(current_user)
Gitlab::SlashCommands::Presenters::Deploy
.new(deployment).present(from, to)
end end
end end
private private
def play!(from, to, action) def find_action(from, to)
action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from) environment = project.environments.find_by(name: from)
return [] unless environment return unless environment
environment.actions_for(to).select(&:starts_environment?) actions = environment.actions_for(to).select do |action|
action.starts_environment?
end
if actions.many?
actions.find { |action| action.name == to.to_s }
else
actions.first
end
end end
end end
end end
......
...@@ -3,17 +3,14 @@ module Gitlab ...@@ -3,17 +3,14 @@ module Gitlab
module Presenters module Presenters
class Deploy < Presenters::Base class Deploy < Presenters::Base
def present(from, to) def present(from, to)
message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." message = "Deployment started from #{from} to #{to}. " \
"[Follow its progress](#{resource_url})."
in_channel_response(text: message) in_channel_response(text: message)
end end
def no_actions def action_not_found
ephemeral_response(text: "No action found to be executed") ephemeral_response(text: "Couldn't find a deployment manual action.")
end
def too_many_actions
ephemeral_response(text: "Too many actions defined")
end end
end end
end end
......
...@@ -413,14 +413,14 @@ msgstr[0] "Fork" ...@@ -413,14 +413,14 @@ msgstr[0] "Fork"
msgstr[1] "Forks" msgstr[1] "Forks"
msgid "ForkedFromProjectPath|Forked from" msgid "ForkedFromProjectPath|Forked from"
msgstr "Forked de" msgstr "Fork criado a partir de"
msgid "From issue creation until deploy to production" msgid "From issue creation until deploy to production"
msgstr "Da abertura de tarefas até a implantação para a produção" msgstr "Da abertura de tarefas até a implantação para a produção"
msgid "From merge request merge until deploy to production" msgid "From merge request merge until deploy to production"
msgstr "" msgstr ""
"Da aceitação da solicitação de incorporação até a implantação em produção" "Do merge request até a implantação em produção"
msgid "Go to your fork" msgid "Go to your fork"
msgstr "Ir para seu fork" msgstr "Ir para seu fork"
...@@ -576,7 +576,7 @@ msgid "NotificationEvent|Successful pipeline" ...@@ -576,7 +576,7 @@ msgid "NotificationEvent|Successful pipeline"
msgstr "Pipeline bem sucedido" msgstr "Pipeline bem sucedido"
msgid "NotificationLevel|Custom" msgid "NotificationLevel|Custom"
msgstr "Personalizar" msgstr "Personalizado"
msgid "NotificationLevel|Disabled" msgid "NotificationLevel|Disabled"
msgstr "Desabilitado" msgstr "Desabilitado"
...@@ -881,9 +881,9 @@ msgid "" ...@@ -881,9 +881,9 @@ msgid ""
"the issue to a milestone, or add the issue to a list on your Issue Board. " "the issue to a milestone, or add the issue to a list on your Issue Board. "
"Begin creating issues to see data for this stage." "Begin creating issues to see data for this stage."
msgstr "" msgstr ""
"A etapa de relatos mostra o tempo que leva desde a criação de uma issue até " "A etapa de planejamento mostra o tempo que se leva desde a criação de uma "
"sua atribuição a um marco, ou sua adição a uma lista no seu Issue Board. " "issue até sua atribuição à um milestone, ou sua adição a uma lista no seu "
"Comece a criar issues para ver dados para esta etapa." "Issue Board. Comece a criar issues para ver dados para esta etapa."
msgid "The phase of the development lifecycle." msgid "The phase of the development lifecycle."
msgstr "A fase do ciclo de vida do desenvolvimento." msgstr "A fase do ciclo de vida do desenvolvimento."
...@@ -974,7 +974,7 @@ msgid "Time before an issue gets scheduled" ...@@ -974,7 +974,7 @@ msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada" msgstr "Tempo até que uma issue seja agendada"
msgid "Time before an issue starts implementation" msgid "Time before an issue starts implementation"
msgstr "Tempo até que uma issue comece a ser implementado" msgstr "Tempo até que uma issue comece a ser implementada"
msgid "Time between merge request creation and merge/close" msgid "Time between merge request creation and merge/close"
msgstr "" msgstr ""
......
...@@ -131,7 +131,7 @@ describe Projects::NotesController do ...@@ -131,7 +131,7 @@ describe Projects::NotesController do
before do before do
sign_in(user) sign_in(user)
project.team << [user, :developer] project.add_developer(user)
end end
it "returns status 302 for html" do it "returns status 302 for html" do
...@@ -165,6 +165,66 @@ describe Projects::NotesController do ...@@ -165,6 +165,66 @@ describe Projects::NotesController do
expect(response).to have_http_status(302) expect(response).to have_http_status(302)
end end
end end
context 'when creating a commit comment from an MR fork' do
let(:project) { create(:project) }
let(:fork_project) do
create(:project).tap do |fork|
create(:forked_project_link, forked_to_project: fork, forked_from_project: project)
end
end
let(:merge_request) do
create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master')
end
let(:existing_comment) do
create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first)
end
def post_create(extra_params = {})
post :create, {
note: { note: 'some other note' },
namespace_id: project.namespace,
project_id: project,
target_type: 'merge_request',
target_id: merge_request.id,
note_project_id: fork_project.id,
in_reply_to_discussion_id: existing_comment.discussion_id
}.merge(extra_params)
end
context 'when the note_project_id is not correct' do
it 'returns a 404' do
post_create(note_project_id: Project.maximum(:id).succ)
expect(response).to have_http_status(404)
end
end
context 'when the user has no access to the fork' do
it 'returns a 404' do
post_create
expect(response).to have_http_status(404)
end
end
context 'when the user has access to the fork' do
let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) }
before do
fork_project.add_developer(user)
existing_comment
end
it 'creates the note' do
expect { post_create }.to change { fork_project.notes.count }.by(1)
end
end
end
end end
describe 'DELETE destroy' do describe 'DELETE destroy' do
......
...@@ -16,12 +16,8 @@ FactoryGirl.define do ...@@ -16,12 +16,8 @@ FactoryGirl.define do
state :closed state :closed
end end
trait :reopened do
state :reopened
end
factory :closed_issue, traits: [:closed] factory :closed_issue, traits: [:closed]
factory :reopened_issue, traits: [:reopened] factory :reopened_issue, traits: [:opened]
factory :labeled_issue do factory :labeled_issue do
transient do transient do
......
...@@ -44,10 +44,6 @@ FactoryGirl.define do ...@@ -44,10 +44,6 @@ FactoryGirl.define do
state :opened state :opened
end end
trait :reopened do
state :reopened
end
trait :locked do trait :locked do
state :locked state :locked
end end
...@@ -74,7 +70,7 @@ FactoryGirl.define do ...@@ -74,7 +70,7 @@ FactoryGirl.define do
factory :merged_merge_request, traits: [:merged] factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed] factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened] factory :reopened_merge_request, traits: [:opened]
factory :merge_request_with_diffs, traits: [:with_diffs] factory :merge_request_with_diffs, traits: [:with_diffs]
factory :merge_request_with_diff_notes do factory :merge_request_with_diff_notes do
after(:create) do |mr| after(:create) do |mr|
......
...@@ -32,11 +32,13 @@ describe 'Admin > Users > Impersonation Tokens', js: true do ...@@ -32,11 +32,13 @@ describe 'Admin > Users > Impersonation Tokens', js: true do
check "api" check "api"
check "read_user" check "read_user"
expect { click_on "Create impersonation token" }.to change { PersonalAccessTokensFinder.new(impersonation: true).execute.count } click_on "Create impersonation token"
expect(active_impersonation_tokens).to have_text(name) expect(active_impersonation_tokens).to have_text(name)
expect(active_impersonation_tokens).to have_text('In') expect(active_impersonation_tokens).to have_text('In')
expect(active_impersonation_tokens).to have_text('api') expect(active_impersonation_tokens).to have_text('api')
expect(active_impersonation_tokens).to have_text('read_user') expect(active_impersonation_tokens).to have_text('read_user')
expect(PersonalAccessTokensFinder.new(impersonation: true).execute.count).to equal(1)
end end
end end
......
...@@ -79,15 +79,7 @@ RSpec.describe 'Dashboard Issues' do ...@@ -79,15 +79,7 @@ RSpec.describe 'Dashboard Issues' do
end end
end end
it 'shows the new issue page', js: true do it 'shows the new issue page', :js do
original_defaults = Gitlab::Application.routes.default_url_options
Gitlab::Application.routes.default_url_options = {
host: Capybara.current_session.server.host,
port: Capybara.current_session.server.port,
protocol: 'http'
}
find('.new-project-item-select-button').trigger('click') find('.new-project-item-select-button').trigger('click')
wait_for_requests wait_for_requests
find('.select2-results li').click find('.select2-results li').click
...@@ -97,8 +89,6 @@ RSpec.describe 'Dashboard Issues' do ...@@ -97,8 +89,6 @@ RSpec.describe 'Dashboard Issues' do
page.within('#content-body') do page.within('#content-body') do
expect(page).to have_selector('.issue-form') expect(page).to have_selector('.issue-form')
end end
Gitlab::Application.routes.default_url_options = original_defaults
end end
end end
end end
...@@ -16,15 +16,13 @@ feature 'Create Branch/Merge Request Dropdown on issue page', js: true do ...@@ -16,15 +16,13 @@ feature 'Create Branch/Merge Request Dropdown on issue page', js: true do
select_dropdown_option('create-mr') select_dropdown_option('create-mr')
wait_for_requests expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
visit project_issue_path(project, issue)
expect(page).to have_content("created branch 1-cherry-coloured-funk") expect(page).to have_content("created branch 1-cherry-coloured-funk")
expect(page).to have_content("mentioned in merge request !1") expect(page).to have_content("mentioned in merge request !1")
visit project_merge_request_path(project, MergeRequest.first)
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
end end
it 'allows creating a branch from the issue page' do it 'allows creating a branch from the issue page' do
......
...@@ -25,6 +25,33 @@ feature 'Merge request created from fork' do ...@@ -25,6 +25,33 @@ feature 'Merge request created from fork' do
expect(page).to have_content 'Test merge request' expect(page).to have_content 'Test merge request'
end end
context 'when a commit comment exists on the merge request' do
given(:comment) { 'A commit comment' }
given(:reply) { 'A reply comment' }
background do
create(:note_on_commit, note: comment,
project: fork_project,
commit_id: merge_request.commit_shas.first)
end
scenario 'user can reply to the comment', js: true do
visit_merge_request(merge_request)
expect(page).to have_content(comment)
page.within('.discussion-notes') do
find('.btn-text-field').click
find('#note_note').send_keys(reply)
find('.comment-btn').click
end
wait_for_requests
expect(page).to have_content(reply)
end
end
context 'source project is deleted' do context 'source project is deleted' do
background do background do
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
......
...@@ -29,7 +29,7 @@ describe 'Branches' do ...@@ -29,7 +29,7 @@ describe 'Branches' do
it 'sorts the branches by name' do it 'sorts the branches by name' do
visit project_branches_path(project) visit project_branches_path(project)
click_button "Name" # Open sorting dropdown click_button "Last updated" # Open sorting dropdown
click_link "Name" click_link "Name"
sorted = repository.branches_sorted_by(:name).first(20).map do |branch| sorted = repository.branches_sorted_by(:name).first(20).map do |branch|
...@@ -41,7 +41,7 @@ describe 'Branches' do ...@@ -41,7 +41,7 @@ describe 'Branches' do
it 'sorts the branches by last updated' do it 'sorts the branches by last updated' do
visit project_branches_path(project) visit project_branches_path(project)
click_button "Name" # Open sorting dropdown click_button "Last updated" # Open sorting dropdown
click_link "Last updated" click_link "Last updated"
sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch|
...@@ -53,7 +53,7 @@ describe 'Branches' do ...@@ -53,7 +53,7 @@ describe 'Branches' do
it 'sorts the branches by oldest updated' do it 'sorts the branches by oldest updated' do
visit project_branches_path(project) visit project_branches_path(project)
click_button "Name" # Open sorting dropdown click_button "Last updated" # Open sorting dropdown
click_link "Oldest updated" click_link "Oldest updated"
sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch|
......
...@@ -12,19 +12,32 @@ describe DiffHelper do ...@@ -12,19 +12,32 @@ describe DiffHelper do
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
describe 'diff_view' do describe 'diff_view' do
it 'uses the view param over the cookie' do
controller.params[:view] = 'parallel'
helper.request.cookies[:diff_view] = 'inline'
expect(helper.diff_view).to eq :parallel
end
it 'returns the default value when the view param is invalid' do
controller.params[:view] = 'invalid'
expect(helper.diff_view).to eq :inline
end
it 'returns a valid value when cookie is set' do it 'returns a valid value when cookie is set' do
helper.request.cookies[:diff_view] = 'parallel' helper.request.cookies[:diff_view] = 'parallel'
expect(helper.diff_view).to eq :parallel expect(helper.diff_view).to eq :parallel
end end
it 'returns a default value when cookie is invalid' do it 'returns the default value when cookie is invalid' do
helper.request.cookies[:diff_view] = 'invalid' helper.request.cookies[:diff_view] = 'invalid'
expect(helper.diff_view).to eq :inline expect(helper.diff_view).to eq :inline
end end
it 'returns a default value when cookie is nil' do it 'returns the default value when cookie is nil' do
expect(helper.request.cookies).to be_empty expect(helper.request.cookies).to be_empty
expect(helper.diff_view).to eq :inline expect(helper.diff_view).to eq :inline
......
...@@ -107,14 +107,6 @@ describe Banzai::Filter::IssuableStateFilter do ...@@ -107,14 +107,6 @@ describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq(issue.to_reference) expect(doc.css('a').last.text).to eq(issue.to_reference)
end end
it 'ignores reopened issue references' do
issue = create_issue(:reopened)
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq(issue.to_reference)
end
it 'appends state to closed issue references' do it 'appends state to closed issue references' do
link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue') link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context) doc = filter(link, context)
...@@ -139,7 +131,7 @@ describe Banzai::Filter::IssuableStateFilter do ...@@ -139,7 +131,7 @@ describe Banzai::Filter::IssuableStateFilter do
end end
it 'ignores reopened merge request references' do it 'ignores reopened merge request references' do
merge_request = create_merge_request(:reopened) merge_request = create_merge_request(:opened)
link = create_link( link = create_link(
merge_request.to_reference, merge_request.to_reference,
......
...@@ -42,13 +42,18 @@ describe Gitlab::QuickActions::Dsl do ...@@ -42,13 +42,18 @@ describe Gitlab::QuickActions::Dsl do
command :with_params_parsing do |parsed| command :with_params_parsing do |parsed|
parsed parsed
end end
params '<Comment>'
substitution :something do |text|
"#{text} Some complicated thing you want in here"
end
end end
end end
describe '.command_definitions' do describe '.command_definitions' do
it 'returns an array with commands definitions' do it 'returns an array with commands definitions' do
no_args_def, explanation_with_aliases_def, dynamic_description_def, no_args_def, explanation_with_aliases_def, dynamic_description_def,
cc_def, cond_action_def, with_params_parsing_def = cc_def, cond_action_def, with_params_parsing_def, substitution_def =
DummyClass.command_definitions DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args) expect(no_args_def.name).to eq(:no_args)
...@@ -104,6 +109,15 @@ describe Gitlab::QuickActions::Dsl do ...@@ -104,6 +109,15 @@ describe Gitlab::QuickActions::Dsl do
expect(with_params_parsing_def.condition_block).to be_nil expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
expect(substitution_def.name).to eq(:something)
expect(substitution_def.aliases).to eq([])
expect(substitution_def.description).to eq('')
expect(substitution_def.explanation).to eq('')
expect(substitution_def.params).to eq(['<Comment>'])
expect(substitution_def.condition_block).to be_nil
expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
expect(substitution_def.parse_params_block).to be_nil
end end
end end
end end
...@@ -9,6 +9,11 @@ describe Gitlab::QuickActions::Extractor do ...@@ -9,6 +9,11 @@ describe Gitlab::QuickActions::Extractor do
command(:assign) { } command(:assign) { }
command(:labels) { } command(:labels) { }
command(:power) { } command(:power) { }
command(:noop_command)
substitution(:substitution) { 'foo' }
substitution :shrug do |comment|
"#{comment} SHRUG"
end
end.command_definitions end.command_definitions
end end
...@@ -177,6 +182,38 @@ describe Gitlab::QuickActions::Extractor do ...@@ -177,6 +182,38 @@ describe Gitlab::QuickActions::Extractor do
expect(msg).to eq "hello\nworld" expect(msg).to eq "hello\nworld"
end end
it 'does not extract noop commands' do
msg = %(hello\nworld\n/reopen\n/noop_command)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen']]
expect(msg).to eq "hello\nworld\n/noop_command"
end
it 'extracts and performs substitution commands' do
msg = %(hello\nworld\n/reopen\n/substitution)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['substitution']]
expect(msg).to eq "hello\nworld\nfoo"
end
it 'extracts and performs substitution commands' do
msg = %(hello\nworld\n/reopen\n/shrug this is great?)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['shrug', 'this is great?']]
expect(msg).to eq "hello\nworld\nthis is great? SHRUG"
end
it 'extracts and performs substitution commands with comments' do
msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['substitution', 'wow this is a thing.']]
expect(msg).to eq "hello\nworld\nfoo"
end
it 'extracts multiple commands' do it 'extracts multiple commands' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen) msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
msg, commands = extractor.extract_commands(msg) msg, commands = extractor.extract_commands(msg)
......
require 'spec_helper'
describe Gitlab::QuickActions::SubstitutionDefinition do
let(:content) do
<<EOF
Hello! Let's do this!
/sub_name I like this stuff
EOF
end
subject do
described_class.new(:sub_name, action_block: proc { |text| "#{text} foo" })
end
describe '#perform_substitution!' do
it 'returns nil if content is nil' do
expect(subject.perform_substitution(self, nil)).to be_nil
end
it 'performs the substitution by default' do
expect(subject.perform_substitution(self, content)).to eq <<EOF
Hello! Let's do this!
I like this stuff foo
EOF
end
end
describe '#match' do
it 'checks the content for the command' do
expect(subject.match(content)).to be_truthy
end
it 'returns the match data' do
data = subject.match(content)
expect(data).to be_a(MatchData)
expect(data[1]).to eq('I like this stuff')
end
it 'is nil if content does not have the command' do
expect(subject.match('blah')).to be_falsey
end
end
end
...@@ -80,7 +80,7 @@ describe Gitlab::SlashCommands::Command do ...@@ -80,7 +80,7 @@ describe Gitlab::SlashCommands::Command do
it 'returns error' do it 'returns error' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to include('Too many actions defined') expect(subject[:text]).to include("Couldn't find a deployment manual action.")
end end
end end
end end
......
...@@ -22,7 +22,7 @@ describe Gitlab::SlashCommands::Deploy do ...@@ -22,7 +22,7 @@ describe Gitlab::SlashCommands::Deploy do
context 'if no environment is defined' do context 'if no environment is defined' do
it 'does not execute an action' do it 'does not execute an action' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed") expect(subject[:text]).to eq "Couldn't find a deployment manual action."
end end
end end
...@@ -35,12 +35,12 @@ describe Gitlab::SlashCommands::Deploy do ...@@ -35,12 +35,12 @@ describe Gitlab::SlashCommands::Deploy do
context 'without actions' do context 'without actions' do
it 'does not execute an action' do it 'does not execute an action' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed") expect(subject[:text]).to eq "Couldn't find a deployment manual action."
end end
end end
context 'with action' do context 'when single action has been matched' do
let!(:manual1) do before do
create(:ci_build, :manual, pipeline: pipeline, create(:ci_build, :manual, pipeline: pipeline,
name: 'first', name: 'first',
environment: 'production') environment: 'production')
...@@ -48,31 +48,61 @@ describe Gitlab::SlashCommands::Deploy do ...@@ -48,31 +48,61 @@ describe Gitlab::SlashCommands::Deploy do
it 'returns success result' do it 'returns success result' do
expect(subject[:response_type]).to be(:in_channel) expect(subject[:response_type]).to be(:in_channel)
expect(subject[:text]).to start_with('Deployment started from staging to production') expect(subject[:text])
.to start_with('Deployment started from staging to production')
end
end end
context 'when duplicate action exists' do context 'when more than one action has been matched' do
let!(:manual2) do context 'when there is no specific actions with a environment name' do
before do
create(:ci_build, :manual, pipeline: pipeline,
name: 'first',
environment: 'production')
create(:ci_build, :manual, pipeline: pipeline, create(:ci_build, :manual, pipeline: pipeline,
name: 'second', name: 'second',
environment: 'production') environment: 'production')
end end
it 'returns error' do it 'returns error about too many actions defined' do
expect(subject[:text]).to eq("Couldn't find a deployment manual action.")
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq('Too many actions defined')
end end
end end
context 'when teardown action exists' do context 'when one of the actions is environement specific action' do
let!(:teardown) do before do
create(:ci_build, :manual, pipeline: pipeline,
name: 'first',
environment: 'production')
create(:ci_build, :manual, pipeline: pipeline,
name: 'production',
environment: 'production')
end
it 'deploys to production' do
expect(subject[:text])
.to start_with('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel)
end
end
context 'when one of the actions is a teardown action' do
before do
create(:ci_build, :manual, pipeline: pipeline,
name: 'first',
environment: 'production')
create(:ci_build, :manual, :teardown_environment, create(:ci_build, :manual, :teardown_environment,
pipeline: pipeline, name: 'teardown', environment: 'production') pipeline: pipeline, name: 'teardown', environment: 'production')
end end
it 'returns the success message' do it 'deploys to production' do
expect(subject[:text])
.to start_with('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel) expect(subject[:response_type]).to be(:in_channel)
expect(subject[:text]).to start_with('Deployment started from staging to production')
end end
end end
end end
......
...@@ -17,8 +17,8 @@ describe Gitlab::SlashCommands::Presenters::Deploy do ...@@ -17,8 +17,8 @@ describe Gitlab::SlashCommands::Presenters::Deploy do
end end
end end
describe '#no_actions' do describe '#action_not_found' do
subject { described_class.new(nil).no_actions } subject { described_class.new(nil).action_not_found }
it { is_expected.to have_key(:text) } it { is_expected.to have_key(:text) }
it { is_expected.to have_key(:response_type) } it { is_expected.to have_key(:response_type) }
...@@ -27,21 +27,7 @@ describe Gitlab::SlashCommands::Presenters::Deploy do ...@@ -27,21 +27,7 @@ describe Gitlab::SlashCommands::Presenters::Deploy do
it 'tells the user there is no action' do it 'tells the user there is no action' do
expect(subject[:response_type]).to be(:ephemeral) expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("No action found to be executed") expect(subject[:text]).to eq "Couldn't find a deployment manual action."
end
end
describe '#too_many_actions' do
subject { described_class.new([]).too_many_actions }
it { is_expected.to have_key(:text) }
it { is_expected.to have_key(:response_type) }
it { is_expected.to have_key(:status) }
it { is_expected.not_to have_key(:attachments) }
it 'tells the user there is no action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to eq("Too many actions defined")
end end
end end
end end
...@@ -21,7 +21,7 @@ describe ProjectWiki do ...@@ -21,7 +21,7 @@ describe ProjectWiki do
describe '#web_url' do describe '#web_url' do
it 'returns the full web URL to the wiki' do it 'returns the full web URL to the wiki' do
expect(subject.web_url).to match("https?://[^\/]+/#{project.path_with_namespace}/wikis/home") expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home")
end end
end end
......
...@@ -1259,7 +1259,7 @@ describe API::Issues do ...@@ -1259,7 +1259,7 @@ describe API::Issues do
put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen' put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen'
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'reopened' expect(json_response['state']).to eq 'opened'
end end
context 'when an admin or owner makes the request' do context 'when an admin or owner makes the request' do
......
...@@ -1114,7 +1114,7 @@ describe API::V3::Issues do ...@@ -1114,7 +1114,7 @@ describe API::V3::Issues do
put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['state']).to eq 'reopened' expect(json_response['state']).to eq 'opened'
end end
context 'when an admin or owner makes the request' do context 'when an admin or owner makes the request' do
......
...@@ -87,7 +87,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -87,7 +87,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "adds hook to project" do it "adds hook to project" do
expect do expect do
post v3_api("/projects/#{project.id}/hooks", user), post v3_api("/projects/#{project.id}/hooks", user),
url: "http://example.com", issues_events: true, wiki_page_events: true url: "http://example.com", issues_events: true, wiki_page_events: true, build_events: true
end.to change {project.hooks.count}.by(1) end.to change {project.hooks.count}.by(1)
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
...@@ -97,7 +97,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -97,7 +97,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(false) expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false) expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false) expect(json_response['build_events']).to eq(true)
expect(json_response['pipeline_events']).to eq(false) expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true) expect(json_response['wiki_page_events']).to eq(true)
expect(json_response['enable_ssl_verification']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true)
...@@ -135,7 +135,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -135,7 +135,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
describe "PUT /projects/:id/hooks/:hook_id" do describe "PUT /projects/:id/hooks/:hook_id" do
it "updates an existing project hook" do it "updates an existing project hook" do
put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user), put v3_api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false url: 'http://example.org', push_events: false, build_events: true
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['url']).to eq('http://example.org') expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events) expect(json_response['issues_events']).to eq(hook.issues_events)
......
...@@ -20,7 +20,7 @@ describe Boards::Issues::ListService do ...@@ -20,7 +20,7 @@ describe Boards::Issues::ListService do
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
let!(:reopened_issue1) { create(:issue, :reopened, project: project) } let!(:reopened_issue1) { create(:issue, :opened, project: project) }
let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) } let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) }
......
...@@ -73,7 +73,7 @@ describe Boards::Issues::MoveService do ...@@ -73,7 +73,7 @@ describe Boards::Issues::MoveService do
issue.reload issue.reload
expect(issue.labels).to contain_exactly(bug, testing) expect(issue.labels).to contain_exactly(bug, testing)
expect(issue).to be_reopened expect(issue).to be_opened
end end
end end
......
...@@ -43,7 +43,7 @@ describe DeleteMergedBranchesService do ...@@ -43,7 +43,7 @@ describe DeleteMergedBranchesService do
context 'open merge requests' do context 'open merge requests' do
it 'does not delete branches from open merge requests' do it 'does not delete branches from open merge requests' do
fork_link = create(:forked_project_link, forked_from_project: project) fork_link = create(:forked_project_link, forked_from_project: project)
create(:merge_request, :reopened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master')
create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master')
service.execute service.execute
......
...@@ -658,8 +658,7 @@ describe GitPushService, services: true do ...@@ -658,8 +658,7 @@ describe GitPushService, services: true do
end end
it 'only schedules a limited number of commits' do it 'only schedules a limited number of commits' do
allow(service).to receive(:push_commits) service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))
.and_return(Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true)))
expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
...@@ -667,8 +666,7 @@ describe GitPushService, services: true do ...@@ -667,8 +666,7 @@ describe GitPushService, services: true do
end end
it "skips commits which don't include cross-references" do it "skips commits which don't include cross-references" do
allow(service).to receive(:push_commits) service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]
.and_return([double(:commit, to_hash: {}, matches_cross_reference_regex?: false)])
expect(ProcessCommitWorker).not_to receive(:perform_async) expect(ProcessCommitWorker).not_to receive(:perform_async)
......
...@@ -78,7 +78,7 @@ describe MergeRequests::GetUrlsService do ...@@ -78,7 +78,7 @@ describe MergeRequests::GetUrlsService do
end end
context 'pushing to existing branch and merge request is reopened' do context 'pushing to existing branch and merge request is reopened' do
let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) } let!(:merge_request) { create(:merge_request, :opened, source_project: project, source_branch: source_branch) }
let(:changes) { existing_branch_changes } let(:changes) { existing_branch_changes }
it_behaves_like 'show_merge_request_url' it_behaves_like 'show_merge_request_url'
end end
......
...@@ -28,7 +28,7 @@ describe MergeRequests::ReopenService do ...@@ -28,7 +28,7 @@ describe MergeRequests::ReopenService do
end end
it { expect(merge_request).to be_valid } it { expect(merge_request).to be_valid }
it { expect(merge_request).to be_reopened } it { expect(merge_request).to be_opened }
it 'executes hooks with reopen action' do it 'executes hooks with reopen action' do
expect(service).to have_received(:execute_hooks) expect(service).to have_received(:execute_hooks)
......
...@@ -694,17 +694,6 @@ describe NotificationService do ...@@ -694,17 +694,6 @@ describe NotificationService do
let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } } let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } } let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the issue's added labels only" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
should_not_email(subscriber_to_label_1)
should_not_email(subscriber_to_group_label_1)
should_not_email(subscriber_to_group_label_2_on_another_project)
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
end
it "emails the current user if they've opted into notifications about their activity" do it "emails the current user if they've opted into notifications about their activity" do
subscriber_to_label_2.notified_of_own_activity = true subscriber_to_label_2.notified_of_own_activity = true
notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
...@@ -721,6 +710,12 @@ describe NotificationService do ...@@ -721,6 +710,12 @@ describe NotificationService do
it "doesn't send email to anyone but subscribers of the given labels" do it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
should_not_email(subscriber_to_label_1)
should_not_email(subscriber_to_group_label_1)
should_not_email(subscriber_to_group_label_2_on_another_project)
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
should_not_email(issue.assignees.first) should_not_email(issue.assignees.first)
should_not_email(issue.author) should_not_email(issue.author)
should_not_email(@u_watcher) should_not_email(@u_watcher)
...@@ -730,12 +725,6 @@ describe NotificationService do ...@@ -730,12 +725,6 @@ describe NotificationService do
should_not_email(@watcher_and_subscriber) should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber) should_not_email(@unsubscriber)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(subscriber_to_label_1)
should_not_email(subscriber_to_group_label_1)
should_not_email(subscriber_to_group_label_2_on_another_project)
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
end end
context 'confidential issues' do context 'confidential issues' do
...@@ -878,11 +867,6 @@ describe NotificationService do ...@@ -878,11 +867,6 @@ describe NotificationService do
end end
describe '#new_merge_request' do describe '#new_merge_request' do
before do
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:new_merge_request, @u_custom_global)
end
it do it do
notification.new_merge_request(merge_request, @u_disabled) notification.new_merge_request(merge_request, @u_disabled)
...@@ -1008,7 +992,7 @@ describe NotificationService do ...@@ -1008,7 +992,7 @@ describe NotificationService do
let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } } let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } } let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the merge request's added labels only" do it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
should_not_email(subscriber_to_label_1) should_not_email(subscriber_to_label_1)
...@@ -1017,11 +1001,6 @@ describe NotificationService do ...@@ -1017,11 +1001,6 @@ describe NotificationService do
should_email(subscriber_1_to_group_label_2) should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2) should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2) should_email(subscriber_to_label_2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
should_not_email(merge_request.assignee) should_not_email(merge_request.assignee)
should_not_email(merge_request.author) should_not_email(merge_request.author)
should_not_email(@u_watcher) should_not_email(@u_watcher)
...@@ -1031,12 +1010,6 @@ describe NotificationService do ...@@ -1031,12 +1010,6 @@ describe NotificationService do
should_not_email(@unsubscriber) should_not_email(@unsubscriber)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
should_not_email(subscriber_to_label_1)
should_not_email(subscriber_to_group_label_1)
should_not_email(subscriber_to_group_label_2_on_another_project)
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
end end
end end
...@@ -1081,12 +1054,12 @@ describe NotificationService do ...@@ -1081,12 +1054,12 @@ describe NotificationService do
should_email(merge_request.assignee) should_email(merge_request.assignee)
should_email(@u_watcher) should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned) should_email(@u_participant_mentioned)
should_email(@subscriber) should_email(@subscriber)
should_email(@watcher_and_subscriber) should_email(@watcher_and_subscriber)
should_email(@u_guest_watcher)
should_email(@u_custom_global)
should_email(@u_guest_custom)
should_not_email(@unsubscriber) should_not_email(@unsubscriber)
should_not_email(@u_participating) should_not_email(@u_participating)
should_not_email(@u_disabled) should_not_email(@u_disabled)
......
...@@ -37,18 +37,18 @@ describe Projects::TransferService do ...@@ -37,18 +37,18 @@ describe Projects::TransferService do
end end
it 'executes system hooks' do it 'executes system hooks' do
expect_any_instance_of(described_class).to receive(:execute_system_hooks) transfer_project(project, user, group) do |service|
expect(service).to receive(:execute_system_hooks)
transfer_project(project, user, group) end
end end
end end
context 'when transfer fails' do context 'when transfer fails' do
let!(:original_path) { project_path(project) } let!(:original_path) { project_path(project) }
def attempt_project_transfer def attempt_project_transfer(&block)
expect do expect do
transfer_project(project, user, group) transfer_project(project, user, group, &block)
end.to raise_error(ActiveRecord::ActiveRecordError) end.to raise_error(ActiveRecord::ActiveRecordError)
end end
...@@ -80,9 +80,9 @@ describe Projects::TransferService do ...@@ -80,9 +80,9 @@ describe Projects::TransferService do
end end
it "doesn't run system hooks" do it "doesn't run system hooks" do
expect_any_instance_of(described_class).not_to receive(:execute_system_hooks) attempt_project_transfer do |service|
expect(service).not_to receive(:execute_system_hooks)
attempt_project_transfer end
end end
end end
...@@ -120,7 +120,11 @@ describe Projects::TransferService do ...@@ -120,7 +120,11 @@ describe Projects::TransferService do
end end
def transfer_project(project, user, new_namespace) def transfer_project(project, user, new_namespace)
Projects::TransferService.new(project, user).execute(new_namespace) service = Projects::TransferService.new(project, user)
yield(service) if block_given?
service.execute(new_namespace)
end end
context 'visibility level' do context 'visibility level' do
......
...@@ -9,13 +9,13 @@ describe QuickActions::InterpretService do ...@@ -9,13 +9,13 @@ describe QuickActions::InterpretService do
let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') } let(:bug) { create(:label, project: project, title: 'Bug') }
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
let(:service) { described_class.new(project, developer) }
before do before do
project.team << [developer, :developer] project.team << [developer, :developer]
end end
describe '#execute' do describe '#execute' do
let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
shared_examples 'reopen command' do shared_examples 'reopen command' do
...@@ -270,6 +270,22 @@ describe QuickActions::InterpretService do ...@@ -270,6 +270,22 @@ describe QuickActions::InterpretService do
end end
end end
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
expect(new_content).to end_with(described_class::SHRUG)
end
end
shared_examples 'tableflip command' do
it 'appends (╯°□°)╯︵ ┻━┻ to the comment' do
new_content, _ = service.execute(content, issuable)
expect(new_content).to end_with(described_class::TABLEFLIP)
end
end
it_behaves_like 'reopen command' do it_behaves_like 'reopen command' do
let(:content) { '/reopen' } let(:content) { '/reopen' }
let(:issuable) { issue } let(:issuable) { issue }
...@@ -775,6 +791,30 @@ describe QuickActions::InterpretService do ...@@ -775,6 +791,30 @@ describe QuickActions::InterpretService do
end end
end end
context '/shrug command' do
it_behaves_like 'shrug command' do
let(:content) { '/shrug people are people' }
let(:issuable) { issue }
end
it_behaves_like 'shrug command' do
let(:content) { '/shrug' }
let(:issuable) { issue }
end
end
context '/tableflip command' do
it_behaves_like 'tableflip command' do
let(:content) { '/tableflip curse your sudden but enviable betrayal' }
let(:issuable) { issue }
end
it_behaves_like 'tableflip command' do
let(:content) { '/tableflip' }
let(:issuable) { issue }
end
end
context '/target_branch command' do context '/target_branch command' do
let(:non_empty_project) { create(:project, :repository) } let(:non_empty_project) { create(:project, :repository) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
......
...@@ -53,7 +53,7 @@ describe WebHookService do ...@@ -53,7 +53,7 @@ describe WebHookService do
end end
it 'handles exceptions' do it 'handles exceptions' do
exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout] exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout]
exceptions.each do |exception_class| exceptions.each do |exception_class|
exception = exception_class.new('Exception message') exception = exception_class.new('Exception message')
......
...@@ -59,6 +59,7 @@ RSpec.configure do |config| ...@@ -59,6 +59,7 @@ RSpec.configure do |config|
config.include Gitlab::Routing, type: :routing config.include Gitlab::Routing, type: :routing
config.include MigrationsHelpers, :migration config.include MigrationsHelpers, :migration
config.include StubFeatureFlags config.include StubFeatureFlags
config.include StubENV
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
......
...@@ -36,7 +36,14 @@ RSpec.configure do |config| ...@@ -36,7 +36,14 @@ RSpec.configure do |config|
$capybara_server_already_started = true $capybara_server_already_started = true
end end
config.after(:each, :js) do |example| config.before(:example, :js) do
allow(Gitlab::Application.routes).to receive(:default_url_options).and_return(
host: Capybara.current_session.server.host,
port: Capybara.current_session.server.port,
protocol: 'http')
end
config.after(:example, :js) do |example|
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by # but `block_and_wait_for_requests_complete` is called before it so by
# calling it explicitely here, we prevent any new requests from being fired # calling it explicitely here, we prevent any new requests from being fired
......
require 'spec_helper'
describe 'shared/projects/_project.html.haml' do
let(:project) { create(:empty_project) }
it 'should render creator avatar if project has a creator' do
render 'shared/projects/project', use_creator_avatar: true, project: project
expect(rendered).to have_selector('img.avatar')
end
it 'should render a generic avatar if project does not have a creator' do
project.creator = nil
render 'shared/projects/project', use_creator_avatar: true, project: project
expect(rendered).to have_selector('.project-avatar')
end
end
...@@ -9,10 +9,17 @@ describe GitGarbageCollectWorker do ...@@ -9,10 +9,17 @@ describe GitGarbageCollectWorker do
subject { described_class.new } subject { described_class.new }
describe "#perform" do describe "#perform" do
it "flushes ref caches when the task is 'gc'" do shared_examples 'flushing ref caches' do |gitaly|
it "flushes ref caches when the task if 'gc'" do
expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
if gitaly
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
.and_return(nil)
else
expect(Gitlab::Popen).to receive(:popen) expect(Gitlab::Popen).to receive(:popen)
.with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
end
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
...@@ -21,6 +28,33 @@ describe GitGarbageCollectWorker do ...@@ -21,6 +28,33 @@ describe GitGarbageCollectWorker do
subject.perform(project.id) subject.perform(project.id)
end end
end
context "with Gitaly turned on" do
it_should_behave_like 'flushing ref caches', true
end
context "with Gitaly turned off", skip_gitaly_mock: true do
it_should_behave_like 'flushing ref caches', false
end
context "repack_full" do
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full)
.and_return(nil)
subject.perform(project.id, :full_repack)
end
end
context "repack_incremental" do
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental)
.and_return(nil)
subject.perform(project.id, :incremental_repack)
end
end
shared_examples 'gc tasks' do shared_examples 'gc tasks' do
before do before do
......
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