Commit 25521a0a authored by Ruben Davila's avatar Ruben Davila

Merge remote-tracking branch 'ce/8-11-stable' into 8-11-stable-ee

parents 4894540a 4bb1db90
Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased)
- Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
- Add Koding (online IDE) integration
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
- Fix adding line comments on the initial commit to a repo !5900
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
......@@ -17,6 +20,7 @@ v 8.11.0 (unreleased)
- API: Endpoints for enabling and disabling deploy keys
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
- Use long options for curl examples in documentation !5703 (winniehell)
- Added tooltip listing label names to the labels value in the collapsed issuable sidebar
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
......@@ -24,6 +28,8 @@ v 8.11.0 (unreleased)
- Ignore URLs starting with // in Markdown links !5677 (winniehell)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
- Fix commit mention font inconsistency (ClemMakesApps)
- Do not escape URI when extracting path !5878 (winniehell)
- Fix filter label tooltip HTML rendering (ClemMakesApps)
- Cache the commit author in RequestStore to avoid extra lookups in PostReceive
- Expand commit message width in repo view (ClemMakesApps)
......@@ -33,6 +39,7 @@ v 8.11.0 (unreleased)
- API: Add deployment endpoints
- API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Show wall clock time when showing a pipeline. !5734
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
......@@ -90,6 +97,7 @@ v 8.11.0 (unreleased)
- The overhead of instrumented method calls has been reduced
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
......@@ -120,9 +128,11 @@ v 8.11.0 (unreleased)
- Change requests_profiles resource constraint to catch virtually any file
- Bump gitlab_git to lazy load compare commits
- Reduce number of queries made for merge_requests/:id/diffs
- Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
- Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
- Fix bug where destroying a namespace would not always destroy projects
- Fix RequestProfiler::Middleware error when code is reloaded in development
- Allow horizontal scrolling of code blocks in issue body
- Catch what warden might throw when profiling requests to re-throw it
- Avoid commit lookup on diff_helper passing existing local variable to the helper method
- Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac)
......@@ -137,6 +147,7 @@ v 8.11.0 (unreleased)
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Add pipelines tab to merge requests
- Fix notification_service argument error of declined invitation emails
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with
- Ensure file editing in UI does not overwrite commited changes without warning user
......
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
<g fill="#d6d7d9">
<path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
<ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
<ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
</g>
</svg>
\ No newline at end of file
......@@ -27,8 +27,6 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
/*= require mousetrap */
......
/*= require_tree . */
(function() {
$(function() {
var url = $(".js-edit-blob-form").data("relative-url-root");
url += $(".js-edit-blob-form").data("assets-prefix");
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
}).call(this);
......@@ -38,7 +38,7 @@ $(() => {
ready () {
Store.disabled = this.disabled;
gl.boardService.all()
.then((resp) => {
.then((resp) => {
resp.json().forEach((board) => {
const list = Store.addList(board);
......
......@@ -55,7 +55,7 @@
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
document.body.classList.remove('is-dragging');
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
......@@ -72,10 +72,6 @@
}
});
if (bp.getBreakpointSize() === 'xs') {
options.handle = '.js-board-drag-handle';
}
this.sortable = Sortable.create(this.$el.parentNode, options);
},
beforeDestroy () {
......
......@@ -63,6 +63,8 @@
Store.moving.issue = card.issue;
Store.moving.list = card.list;
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
......@@ -72,10 +74,6 @@
}
});
if (bp.getBreakpointSize() === 'xs') {
options.handle = '.js-card-drag-handle';
}
this.sortable = Sortable.create(this.$els.list, options);
// Scroll event on list to load more
......
......@@ -2,6 +2,19 @@
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.onStart = () => {
$('.has-tooltip').tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging');
};
gl.issueBoards.onEnd = () => {
$('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging');
};
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = {
forceFallback: true,
......@@ -9,14 +22,11 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip',
scrollSensitivity: 100,
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart () {
document.body.classList.add('is-dragging');
},
onEnd () {
document.body.classList.remove('is-dragging');
}
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
}
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
......
......@@ -3,6 +3,7 @@ class ListLabel {
this.id = obj.id;
this.title = obj.title;
this.color = obj.color;
this.textColor = obj.text_color;
this.description = obj.description;
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
......
......@@ -20,6 +20,9 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
case 'projects:boards:show':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
......@@ -126,10 +129,12 @@
new NotificationsDropdown();
break;
case 'groups:group_members:index':
new gl.MemberExpirationDate();
new GroupMembers();
new UsersSelect();
break;
case 'projects:project_members:index':
new gl.MemberExpirationDate();
new ProjectMembers();
new UsersSelect();
break;
......@@ -171,6 +176,7 @@
new BuildArtifacts();
break;
case 'projects:group_links:index':
new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
......
......@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
......@@ -21,6 +21,7 @@
$block = $selectbox.closest('.block');
$form = $dropdown.closest('form');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL != null) {
......@@ -31,7 +32,11 @@
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
}
saveLabelData = function() {
var data, selected;
......@@ -52,7 +57,7 @@
dataType: 'JSON',
data: data
}).done(function(data) {
var labelCount, template;
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
......@@ -66,6 +71,27 @@
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
labelTooltipTitle = labelTitles.join(', ');
} else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
.tooltip('fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
});
......
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
(function() {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
gl.MemberExpirationDate = function() {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
var inputs = $('.js-access-expiration-date');
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
onSelect: toggleClearInput
});
inputs.next('.js-clear-input').on('click', function(event) {
event.preventDefault();
var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
input.datepicker('setDate', null);
toggleClearInput.call(input);
});
inputs.on('blur', toggleClearInput);
inputs.each(toggleClearInput);
};
}).call(this);
......@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
return ProjectMembers;
})();
}).call(this);
/*= require_tree . */
(function() {
$(function() {
var editor = ace.edit("editor")
$(".snippet-form-holder form").on('submit', function() {
$(".snippet-file-content").val(editor.getValue());
});
});
}).call(this);
......@@ -14,12 +14,20 @@
margin-top: 0;
}
// Single code lines should wrap
code {
font-family: $monospace_font;
white-space: pre;
white-space: pre-wrap;
word-wrap: normal;
}
// Multi-line code blocks should scroll horizontally
pre {
code {
white-space: pre;
}
}
kbd {
display: inline-block;
padding: 3px 5px;
......
......@@ -8,9 +8,13 @@
}
.is-dragging {
// Important because plugin sets inline CSS
opacity: 1!important;
* {
cursor: -webkit-grabbing;
cursor: grabbing;
// !important to make sure no style can override this when dragging
cursor: -webkit-grabbing!important;
cursor: grabbing!important;
}
}
......@@ -101,8 +105,8 @@
.board {
display: -webkit-flex;
display: flex;
min-width: calc(100vw - 15px);
max-width: calc(100vw - 15px);
min-width: calc(85vw - 15px);
max-width: calc(85vw - 15px);
margin-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
......@@ -154,14 +158,6 @@
padding: $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
.board-mobile-handle {
position: relative;
left: 0;
top: 1px;
margin-top: 0;
margin-right: 5px;
}
}
.board-search-container {
......@@ -254,11 +250,6 @@
opacity: 0.3;
}
.is-dragging {
// Important because plugin sets inline CSS
opacity: 1!important;
}
.card {
position: relative;
width: 100%;
......@@ -269,11 +260,7 @@
list-style: none;
&.user-can-drag {
padding-left: ($gl-padding * 2);
@media (min-width: $screen-sm-min) {
padding-left: $gl-padding;
}
padding-left: $gl-padding;
}
&:not(:last-child) {
......@@ -294,17 +281,6 @@
}
}
.board-mobile-handle {
position: absolute;
left: 10px;
top: 50%;
margin-top: (-15px / 2);
@media (min-width: $screen-sm-min) {
display: none;
}
}
.card-title {
margin: 0;
font-size: 1em;
......@@ -316,6 +292,7 @@
.card-footer {
margin-top: 5px;
line-height: 25px;
.label {
margin-right: 4px;
......
......@@ -168,7 +168,6 @@
text-overflow: ellipsis;
&:hover {
background-color: $row-hover;
color: $gl-text-color;
}
}
......@@ -190,6 +189,10 @@
display: block;
}
}
&:hover {
background-color: $row-hover;
}
}
}
}
......
......@@ -66,6 +66,15 @@
margin-left: 8px;
}
}
.ci-status-link {
svg {
position: relative;
top: 2px;
margin: 0 2px 0 3px;
}
}
}
.ci-status-link {
......
......@@ -34,11 +34,4 @@
}
}
}
.wiki {
code {
white-space: pre-wrap;
word-break: keep-all;
}
}
}
......@@ -374,3 +374,10 @@
}
}
}
.merge-request-details {
.title {
margin-bottom: 20px;
}
}
......@@ -300,6 +300,17 @@
&.playable {
background-color: $gray-light;
svg {
height: 12px;
width: 12px;
position: relative;
top: 1px;
path {
fill: $layout-link-gray;
}
}
}
.build-content {
......@@ -319,10 +330,6 @@
margin-right: 5px;
}
.fa {
font-size: 13px;
}
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
......
......@@ -739,3 +739,29 @@ pre.light-well {
width: 300px;
}
}
.clearable-input {
position: relative;
.clear-icon {
@extend .fa-times;
display: none;
position: absolute;
right: 7px;
top: 7px;
color: $location-icon-color;
&:before {
font-family: FontAwesome;
font-weight: normal;
font-style: normal;
}
}
&.has-value {
.clear-icon {
cursor: pointer;
display: block;
}
}
}
......@@ -113,6 +113,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
:koding_enabled,
:koding_url,
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
......
......@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
@group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
@group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
......
......@@ -7,11 +7,16 @@ module ServiceParams
:build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
# We're using `issues_events` and `merge_requests_events`
# in the view so we still need to explicitly state them
# here. `Service#event_names` would only give
# `issue_events` and `merge_request_events` (singular!)
# See app/helpers/services_helper.rb for how we
# make those event names plural as special case.
:issues_events, :merge_requests_events,
:notify_only_broken_builds, :notify_only_broken_pipelines,
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
......@@ -19,9 +24,7 @@ module ServiceParams
FILTER_BLANK_PARAMS = [:password]
def service_params
dynamic_params = []
dynamic_params.concat(@service.event_channel_names)
dynamic_params = @service.event_channel_names + @service.event_names
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
if service_params[:service].is_a?(Hash)
......
......@@ -21,10 +21,15 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
access_level = params[:access_level]
user_ids = params[:user_ids].split(',')
@group.add_users(user_ids, access_level, current_user)
@group.add_users(
user_ids,
params[:access_level],
current_user: current_user,
expires_at: params[:expires_at]
)
group_members = @group.group_members.where(user_id: user_ids)
group_members.each do |group_member|
......@@ -76,7 +81,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
......
class KodingController < ApplicationController
before_action :check_integration!, :authenticate_user!, :reject_blocked!
layout 'koding'
def index
path = File.join(Rails.root, 'doc/integration/koding-usage.md')
@markdown = File.read(path)
end
private
def check_integration!
render_404 unless current_application_settings.koding_enabled?
end
end
......@@ -12,7 +12,7 @@ module Projects
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority] }
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
......
......@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
group: group, group_access: params[:link_group_access]
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
......
......@@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(
:build_events,
:pipeline_events,
:enable_ssl_verification,
:issues_events,
:merge_requests_events,
......
......@@ -36,7 +36,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
@project.team.add_users(
params[:user_ids].split(','),
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
)
members = @project.project_members.where(user_id: params[:user_ids].split(','))
members.each do |member|
......@@ -105,7 +111,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
params.require(:project_member).permit(:user_id, :access_level)
params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
......
......@@ -35,6 +35,10 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
def koding_enabled?
current_application_settings.koding_enabled?
end
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
......
......@@ -217,4 +217,12 @@ module BlobHelper
def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
def blob_editor_paths
{
'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-language' => @blob && @blob.language.try(:ace_mode)
}
end
end
......@@ -39,7 +39,7 @@ module CiStatusHelper
when 'running'
'icon_status_running'
when 'play'
return icon('play fw')
'icon_play'
when 'created'
'icon_status_pending'
else
......
......@@ -72,6 +72,15 @@ module IssuablesHelper
end
end
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index{ |_, i| i < limit }
label_names = first.collect(&:name)
label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ')
end
private
def sidebar_gutter_collapsed?
......
......@@ -245,6 +245,60 @@ module ProjectsHelper
)
end
def add_koding_stack_path(project)
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch || 'master',
file_name: '.koding.yml',
commit_message: "Add Koding stack script",
content: <<-CONTENT.strip_heredoc
provider:
aws:
access_key: '${var.aws_access_key}'
secret_key: '${var.aws_secret_key}'
resource:
aws_instance:
#{project.path}-vm:
instance_type: t2.nano
user_data: |-
# Created by GitLab UI for :>
echo _KD_NOTIFY_@Installing Base packages...@
apt-get update -y
apt-get install git -y
echo _KD_NOTIFY_@Cloning #{project.name}...@
export KODING_USER=${var.koding_user_username}
export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
export BRANCH=${var.koding_queryString_branch}
sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
echo _KD_NOTIFY_@#{project.name} cloned.@
CONTENT
)
end
def koding_project_url(project = nil, branch = nil, sha = nil)
if project
import_path = "/Home/Stacks/import"
repo = project.path_with_namespace
branch ||= project.default_branch
sha ||= project.commit.short_id
path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
return URI.join(current_application_settings.koding_url, path).to_s
end
current_application_settings.koding_url
end
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
......
......@@ -15,20 +15,9 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
def duration_in_numbers(finished_at, started_at)
interval = interval_in_seconds(started_at, finished_at)
time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S"
def duration_in_numbers(duration)
time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
Time.at(interval).utc.strftime(time_format)
end
private
def interval_in_seconds(started_at, finished_at = nil)
if started_at && finished_at
finished_at.to_i - started_at.to_i
elsif started_at
Time.now.to_i - started_at.to_i
end
Time.at(duration).utc.strftime(time_format)
end
end
......@@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
validates :koding_url,
presence: true,
if: :koding_enabled
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......@@ -157,6 +161,8 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
koding_enabled: false,
koding_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
......
......@@ -62,6 +62,7 @@ module Ci
status_event: 'enqueue'
)
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
......@@ -344,7 +345,7 @@ module Ci
def execute_hooks
return unless project
build_data = Gitlab::BuildDataBuilder.build(self)
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
PagesService.new(build_data).execute
......
......@@ -19,6 +19,8 @@ module Ci
after_save :keep_around_commits
delegate :stages, to: :statuses
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
......@@ -56,6 +58,10 @@ module Ci
before_transition do |pipeline|
pipeline.update_duration
end
after_transition do |pipeline, transition|
pipeline.execute_hooks unless transition.loopback?
end
end
# ref can't be HEAD or SHA, can only be branch/tag name
......@@ -72,6 +78,10 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages
end
def self.total_duration
where.not(duration: nil).sum(:duration)
end
def stages_with_latest_statuses
statuses.latest.order(:stage_idx).group_by(&:stage)
end
......@@ -140,6 +150,10 @@ module Ci
end
end
def mark_as_processable_after_stage(stage_idx)
builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
end
def latest?
return false unless ref
commit = project.commit(ref)
......@@ -244,11 +258,21 @@ module Ci
end
def update_duration
self.duration = statuses.latest.duration
self.duration = calculate_duration
end
def execute_hooks
data = pipeline_data
project.execute_hooks(data, :pipeline_hooks)
project.execute_services(data, :pipeline_hooks)
end
private
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
def latest_builds_status
return 'failed' unless yaml_errors.blank?
......
......@@ -229,7 +229,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
base_sha: self.parent_id || self.sha,
base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
head_sha: self.sha
)
end
......
......@@ -21,6 +21,7 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
......@@ -30,6 +31,10 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :skipped] => :pending
end
event :process do
transition skipped: :created
end
event :run do
transition pending: :running
end
......@@ -107,13 +112,7 @@ class CommitStatus < ActiveRecord::Base
end
def duration
duration =
if started_at && finished_at
finished_at - started_at
elsif started_at
Time.now - started_at
end
duration
calculate_duration
end
def stuck?
......
module Expirable
extend ActiveSupport::Concern
included do
scope :expired, -> { where('expires_at <= ?', Time.current) }
end
def expires?
expires_at.present?
end
def expires_soon?
expires_at < 7.days.from_now
end
end
......@@ -17,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
def original_line_code
raise NotImplementedError
end
def diff_attributes
raise NotImplementedError
end
......
......@@ -35,11 +35,6 @@ module Statuseable
all.pluck(self.status_sql).first
end
def duration
duration_array = all.map(&:duration).compact
duration_array.reduce(:+)
end
def started_at
all.minimum(:started_at)
end
......@@ -85,4 +80,14 @@ module Statuseable
def complete?
COMPLETED_STATUSES.include?(status)
end
private
def calculate_duration
if started_at && finished_at
finished_at - started_at
elsif started_at
Time.now - started_at
end
end
end
......@@ -57,6 +57,10 @@ class DiffNote < Note
diff_file.position(line) == self.original_position
end
def original_line_code
self.diff_file.line_code(self.diff_line)
end
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
......
......@@ -12,6 +12,7 @@ class Discussion
:for_merge_request?,
:line_code,
:original_line_code,
:diff_file,
:for_line?,
:active?,
......
......@@ -107,34 +107,41 @@ class Group < Namespace
end
end
def add_users(user_ids, access_level, current_user = nil, skip_notification: false)
def add_users(user_ids, access_level, current_user: nil, skip_notification: false, expires_at: nil)
user_ids.each do |user_id|
Member.add_user(self.group_members, user_id, access_level, current_user, skip_notification: skip_notification)
Member.add_user(
self.group_members,
user_id,
access_level,
current_user: current_user,
skip_notification: skip_notification,
expires_at: expires_at
)
end
end
def add_user(user, access_level, current_user = nil, skip_notification: false)
add_users([user], access_level, current_user, skip_notification: skip_notification)
def add_user(user, access_level, current_user: nil, skip_notification: false, expires_at: nil)
add_users([user], access_level, current_user: current_user, skip_notification: skip_notification, expires_at: expires_at)
end
def add_owner(user, current_user = nil, skip_notification: false)
self.add_user(user, Gitlab::Access::OWNER, current_user, skip_notification: skip_notification)
add_user(user, Gitlab::Access::OWNER, current_user: current_user, skip_notification: skip_notification)
end
def add_guest(user, current_user = nil)
add_user(user, Gitlab::Access::GUEST, current_user)
add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
add_user(user, Gitlab::Access::REPORTER, current_user)
add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
add_user(user, Gitlab::Access::DEVELOPER, current_user)
add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
add_user(user, Gitlab::Access::MASTER, current_user)
add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def has_owner?(user)
......
......@@ -9,5 +9,6 @@ class ProjectHook < WebHook
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
......@@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
default_value_for :build_events, false
default_value_for :pipeline_events, false
default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) }
......
......@@ -49,6 +49,10 @@ class LegacyDiffNote < Note
!line.meta? && diff_file.line_code(line) == self.line_code
end
def original_line_code
self.line_code
end
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
......
class Member < ActiveRecord::Base
include Sortable
include Importable
include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
......@@ -74,7 +75,7 @@ class Member < ActiveRecord::Base
user
end
def add_user(members, user_id, access_level, current_user = nil, skip_notification: false)
def add_user(members, user_id, access_level, current_user: nil, skip_notification: false, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
......@@ -88,7 +89,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
member.expires_at = expires_at
member.skip_notification = skip_notification
member.save
......
......@@ -34,7 +34,7 @@ class ProjectMember < Member
# :master
# )
#
def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
......@@ -50,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
Member.add_user(project.project_members, user, access_level, current_user)
Member.add_user(
project.project_members,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
......
......@@ -1110,8 +1110,8 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
def add_user(user, access_level, current_user = nil)
team.add_user(user, access_level, current_user)
def add_user(user, access_level, current_user: nil, expires_at: nil)
team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
......
class ProjectGroupLink < ActiveRecord::Base
include Expirable
GUEST = 10
REPORTER = 20
DEVELOPER = 30
......@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
private
private
def different_group
if self.group && self.project && self.project.group == self.group
......
......@@ -51,8 +51,7 @@ class BuildsEmailService < Service
end
def test_data(project = nil, user = nil)
build = project.builds.last
Gitlab::BuildDataBuilder.build(build)
Gitlab::DataBuilder::Build.build(project.builds.last)
end
def fields
......
......@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
add_users(users, access, current_user)
add_users(users, access, current_user: current_user)
else
add_user(users, access, current_user)
add_user(users, access, current_user: current_user)
end
end
......@@ -33,19 +33,20 @@ class ProjectTeam
member
end
def add_users(users, access, current_user = nil)
def add_users(users, access, current_user: nil, expires_at: nil)
return false if group_member_lock
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
current_user
current_user: current_user,
expires_at: expires_at
)
end
def add_user(user, access, current_user = nil)
add_users([user], access, current_user)
def add_user(user, access, current_user: nil, expires_at: nil)
add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
......
......@@ -342,7 +342,7 @@ class Repository
def cache_keys
%i(size commit_count
readme version contribution_guide changelog
license_blob license_key gitignore)
license_blob license_key gitignore koding_yml)
end
# Keys for data on branch/tag operations.
......@@ -618,6 +618,14 @@ class Repository
end
end
def koding_yml
return nil unless head_exists?
cache.fetch(:koding_yml) do
file_on_head(/\A\.koding\.yml\z/)
end
end
def gitlab_ci_yml
return nil unless head_exists?
......
......@@ -36,6 +36,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
......@@ -79,13 +80,17 @@ class Service < ActiveRecord::Base
end
def test_data(project, user)
Gitlab::PushDataBuilder.build_sample(project, user)
Gitlab::DataBuilder::Push.build_sample(project, user)
end
def event_channel_names
[]
end
def event_names
supported_events.map { |event| "#{event}_events" }
end
def event_field(event)
nil
end
......
......@@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
end
def build_push_data(branch)
Gitlab::PushDataBuilder
.build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
Gitlab::DataBuilder::Push.build(
project,
current_user,
branch.target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
[])
end
end
......@@ -33,7 +33,12 @@ class DeleteTagService < BaseService
end
def build_push_data(tag)
Gitlab::PushDataBuilder
.build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
Gitlab::DataBuilder::Push.build(
project,
current_user,
tag.target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
[])
end
end
......@@ -143,13 +143,23 @@ class GitPushService < BaseService
end
def build_push_data
@push_data ||= Gitlab::PushDataBuilder.
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
@push_data ||= Gitlab::DataBuilder::Push.build(
@project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
push_commits)
end
def build_push_data_system_hook
@push_data_system ||= Gitlab::PushDataBuilder.
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
@push_data_system ||= Gitlab::DataBuilder::Push.build(
@project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
[])
end
def push_to_existing_branch?
......
......@@ -34,12 +34,24 @@ class GitTagPushService < BaseService
end
end
Gitlab::PushDataBuilder.
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
Gitlab::DataBuilder::Push.build(
project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
commits,
message)
end
def build_system_push_data
Gitlab::PushDataBuilder.
build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
Gitlab::DataBuilder::Push.build(
project,
current_user,
params[:oldrev],
params[:newrev],
params[:ref],
[],
'')
end
end
module Members
class AuthorizedDestroyService < BaseService
attr_accessor :member, :user
def initialize(member, user = nil)
@member, @user = member, user
end
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
end
end
end
......@@ -11,12 +11,7 @@ module Members
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
member.destroy
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
AuthorizedDestroyService.new(member, current_user).execute
end
end
end
......@@ -16,7 +16,7 @@ module Notes
end
def hook_data
Gitlab::NoteDataBuilder.build(@note, @note.author)
Gitlab::DataBuilder::Note.build(@note, @note.author)
end
def execute_note_hooks
......
......@@ -255,7 +255,6 @@ class NotificationService
project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
project_member.access_level,
project_member.created_by_id
).deliver_later
end
......@@ -282,7 +281,6 @@ class NotificationService
group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
group_member.access_level,
group_member.created_by_id
).deliver_later
end
......
class TestHookService
def execute(hook, current_user)
data = Gitlab::PushDataBuilder.build_sample(project(hook), current_user)
data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user)
hook.execute(data, 'push_hooks')
end
......
......@@ -439,6 +439,25 @@
.col-sm-10
= f.text_field :elasticsearch_port, class: 'form-control', placeholder: ApplicationSetting.current.elasticsearch_port
%fieldset
%legend Koding
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :koding_enabled do
= f.check_box :koding_enabled
Enable Koding
.form-group
= f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
.help-block
Koding has integration enabled out of the box for the
%strong gitlab
team, and you need to provide that team's URL here. Learn more in the
= succeed "." do
= link_to "Koding integration documentation", help_page_path("integration/koding")
.form-actions
= f.submit 'Save', class: 'btn btn-save'
......@@ -51,7 +51,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(build.finished_at, build.started_at)
= duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
......
......@@ -7,7 +7,7 @@
.diff-content.code.js-syntax-highlight
%table
- discussions = { discussion.line_code => discussion }
- discussions = { discussion.original_line_code => discussion }
= render partial: "projects/diffs/line",
collection: discussion.truncated_diff_lines,
as: :line,
......
......@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
.form-group
= f.label :expires_at, 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
%i.clear-icon.js-clear-input
.help-block
On this date, the user(s) will automatically lose access to this group and all of its projects.
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
new MemberExpirationDate();
.row-content-block.second-block.center
%p
= icon('circle', class: 'cgreen')
Integration is active for
= link_to koding_project_url, target: '_blank' do
#{current_application_settings.koding_url}
.documentation.wiki
= markdown @markdown
- page_title "Koding"
- page_description "Koding Dashboard"
- header_title "Koding", koding_path
= render template: "layouts/application"
......@@ -12,6 +12,11 @@
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
%span
Activity
- if koding_enabled?
= nav_link(controller: :koding) do
= link_to koding_path, title: 'Koding' do
%span
Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
%span
......
......@@ -65,7 +65,7 @@
Graphs
- if project_nav_tab? :issues
= nav_link(controller: [:issues, :labels, :milestones]) do
= nav_link(controller: [:issues, :labels, :milestones, :boards]) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
......
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
- if @conflict
.alert.alert-danger
......@@ -16,14 +19,10 @@
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= editing_preview_title(@blob.name)
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
:javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
new NewCommitForm($('.js-edit-blob-form'))
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
%h3.page-title
New File
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
:javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
new NewCommitForm($('.js-new-blob-form'))
......@@ -11,7 +11,6 @@
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
= icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }}
......
......@@ -9,7 +9,6 @@
"track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled }",
":index" => "index" }
= icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
......
- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
- if @repository.koding_yml
= link_to koding_project_url(@project), class: 'btn', target: '_blank' do
Run in IDE (Koding)
- else
= link_to add_koding_stack_path(@project), class: 'btn' do
Set Up Koding
......@@ -63,7 +63,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(build.finished_at, build.started_at)
= duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
= icon("calendar")
......
......@@ -48,10 +48,10 @@
\-
%td
- if pipeline.started_at && pipeline.finished_at
- if pipeline.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(pipeline.finished_at, pipeline.started_at)
= duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
......
......@@ -56,10 +56,10 @@
= pluralize(@commit.pipelines.count, 'pipeline')
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
= ci_label_for_status(@commit.status)
- if @commit.pipelines.duration
in
= time_interval_in_words @commit.pipelines.duration
%span.ci-status-label
= ci_label_for_status(@commit.status)
in
= time_interval_in_words @commit.pipelines.total_duration
.commit-box.content-block
%h3.commit-title
......
......@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
%i.clear-icon.js-clear-input
.help-block
On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
......@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
- if group_link.expires?
·
%span{ class: ('text-warning' if group_link.expires_soon?) }
expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
......
......@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger|
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
......
......@@ -16,6 +16,9 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
- if koding_enabled? && @repository.koding_yml
= link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
......
......@@ -9,7 +9,7 @@
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
= time_interval_in_words @pipeline.duration
= time_interval_in_words(@pipeline.duration)
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
......
......@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
.form-group
= f.label :expires_at, 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
%i.clear-icon.js-clear-input
.help-block
On this date, the user(s) will automatically lose access to this project.
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
......@@ -4,7 +4,7 @@
.alert.alert-warning
Adding new users is disabled at group level
.project-members-page.prepend-top-default
.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project) && !membership_locked?
.panel.panel-default
.panel-heading
......
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
new MemberExpirationDate();
- @no_container = true
- page_title "Edit", @tag.name, "Tags"
= render "projects/commits/head"
.row-content-block
.oneline
.title
Release notes for tag
%strong #{@tag.name}
%div{ class: container_class }
.sub-header-block.no-bottom-space
.oneline
.title
Release notes for tag
%strong #{@tag.name}
.prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
.error-alert
.form-actions.prepend-top-default
.prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
......@@ -64,10 +64,12 @@
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render "projects/buttons/download"
......@@ -86,4 +88,4 @@
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
= render default_project_view
\ No newline at end of file
= render default_project_view
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
\ No newline at end of file
......@@ -109,7 +109,7 @@
- if issuable.project.labels.any?
.block.labels
.sidebar-collapsed-icon
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
= issuable.labels_array.size
......
......@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
title: 'Edit access level'
title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
......@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
·
%span{ class: ('text-warning' if member.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
......@@ -73,8 +77,16 @@
- if show_roles
.edit-member.hide.js-toggle-content
%br
= form_for member, remote: true do |f|
.prepend-top-10
= f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
= form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
.form-group
= label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
.col-sm-10
= f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
.form-group
= label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
.col-sm-10
.clearable-input
= f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
%i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_tag('snippet/snippet_bundle.js')
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
= form_errors(@snippet)
......@@ -31,8 +35,3 @@
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
:javascript
var editor = ace.edit("editor");
$(".snippet-form-holder form").submit(function(){
$(".snippet-file-content").val(editor.getValue());
});
......@@ -29,49 +29,56 @@
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
This URL will be triggered by a push to the repository
%li
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
This URL will be triggered when a new tag is pushed to the repository
%li
= f.check_box :note_events, class: 'pull-left'
.prepend-left-20
= f.label :note_events, class: 'list-label' do
%strong Comments
%p.light
This url will be triggered when someone adds a comment
This URL will be triggered when someone adds a comment
%li
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This url will be triggered when an issue is created/updated/merged
This URL will be triggered when an issue is created/updated/merged
%li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created/updated/merged
This URL will be triggered when a merge request is created/updated/merged
%li
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Build events
%p.light
This url will be triggered when the build status changes
This URL will be triggered when the build status changes
%li
= f.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
= f.label :pipeline_events, class: 'list-label' do
%strong Pipeline events
%p.light
This URL will be triggered when the pipeline status changes
%li
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
This url will be triggered when a wiki page is created/updated
This URL will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox
......
......@@ -33,13 +33,13 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
compare = CompareService.new.execute(project, before_sha, project, after_sha)
compare = CompareService.new.execute(project, after_sha, project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
compare = CompareService.new.execute(project, after_sha, project, before_sha)
compare = CompareService.new.execute(project, before_sha, project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
......
class RemoveExpiredGroupLinksWorker
include Sidekiq::Worker
def perform
ProjectGroupLink.expired.destroy_all
end
end
class RemoveExpiredMembersWorker
include Sidekiq::Worker
def perform
Member.expired.find_each do |member|
begin
Members::AuthorizedDestroyService.new(member).execute
rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
end
end
end
......@@ -91,6 +91,8 @@ module Gitlab
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
......
......@@ -384,6 +384,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
#
# GitLab Shell
......
......@@ -98,6 +98,11 @@ Rails.application.routes.draw do
get 'help/ui' => 'help#ui'
get 'help/*path' => 'help#show', as: :help_page
#
# Koding route
#
get 'koding' => 'koding#index'
#
# Global snippets
#
......
class AddPipelineEventsToWebHooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:web_hooks, :pipeline_events, :boolean,
default: false, allow_null: false)
end
def down
remove_column(:web_hooks, :pipeline_events)
end
end
class AddPipelineEventsToServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :pipeline_events, :boolean,
default: false, allow_null: false)
end
def down
remove_column(:services, :pipeline_events)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddExpiresAtToMember < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :members, :expires_at, :date
end
end
class AddKodingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :koding_enabled, :boolean
add_column :application_settings, :koding_url, :string
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :project_group_links, :expires_at, :date
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160817154936) do
ActiveRecord::Schema.define(version: 20160818205718) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -98,6 +98,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
t.boolean "usage_ping_enabled", default: true, null: false
t.boolean "koding_enabled"
t.string "koding_url"
end
create_table "approvals", force: :cascade do |t|
......@@ -643,6 +645,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
......@@ -892,6 +895,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
......@@ -1078,6 +1082,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "category", default: "common", null: false
t.boolean "default", default: false
t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
......@@ -1295,6 +1300,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.boolean "build_events", default: false, null: false
t.string "token"
t.boolean "wiki_page_events", default: false, null: false
t.boolean "pipeline_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
......
......@@ -86,7 +86,8 @@ Example response:
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
"access_level": 30,
"expires_at": null
}
```
......@@ -106,6 +107,7 @@ POST /projects/:id/members
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
......@@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
......
......@@ -67,6 +67,8 @@ use following Markdown code to embed the est coverage report into `README.md`:
![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
```
The latest successful pipeline will be used to read the test coverage value.
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
......
......@@ -17,6 +17,7 @@ See the documentation below for details on how to configure these services.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
- [Koding](koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
......
# Koding & GitLab
This document will guide you through using Koding integration on GitLab in
detail. For configuring and installing please follow [this](koding.md) guide.
You can use Koding integration to run and develop your projects on GitLab. This
will allow you and the users to test your project without leaving the browser.
Koding handles projects as stacks which are basic recipes to define your
environment for your project. With this integration you can automatically
create a proper stack template for your projects. Currently auto-generated
stack templates are designed to work with AWS which requires a valid AWS
credential to be able to use these stacks. You can find more information about
stacks and the other providers that you can use on Koding from
[here](https://www.koding.com/docs).
# Enable Integration
You can enable Koding integration by providing the running Koding instance URL
in Application Settings;
- Open **Admin area > Settings** (`/admin/application_settings`).
![Enable Koding](help/integration/img/koding_enable-koding.png)
Once enabled you will see `Koding` link on your sidebar which leads you to
Koding Landing page
![Koding Landing](help/integration/img/koding_landing.png)
You can navigate to running Koding instance from here. For more information and
details about configuring integration please follow [this](koding.md) guide.
# Set up Koding on Projects
Once it's enabled, you will see some integration buttons on Project pages,
Merge Requests etc. To get started working on a specific project you first need
to create a `.koding.yml` file under your project root. You can easily do that
by using `Set Up Koding` button which will be visible on every project's
landing page;
![Set Up Koding](help/integration/img/koding_set-up-ide.png)
Once you click this will open a New File page on GitLab with auto-generated
`.koding.yml` content based on your server and repository configuration.
![Commit .koding.yml](help/integration/img/koding_commit-koding.yml.png)
# Run a project on Koding
If there is `.koding.yml` exists in your project root, you will see
`Run in IDE (Koding)` button in your project landing page. You can initiate the
process from here.
![Run on Koding](help/integration/img/koding_run-in-ide.png)
This will open Koding defined in the settings in a new window and will start
importing the project's stack file;
![Import Stack](help/integration/img/koding_stack-import.png)
You should see the details of your repository imported into your Koding
instance. Once it's completed it will lead you to the Stack Editor and from
there you can start using your new stack integrated with your project on your
GitLab instance. For details about what's next you can follow
[this](https://www.koding.com/docs/creating-an-aws-stack) guide from 8. step.
Once stack initialized you will see the `README.md` content from your project
in `Stack Build` wizard, this wizard will let you to build the stack and import
your project into it. **Once it's completed it will automatically open the
related vm instead of importing from scratch**
![Stack Building](help/integration/img/koding_start-build.png)
This will take time depending on the required environment.
![Stack Building in Progress](help/integration/img/koding_build-in-progress.png)
It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
AWS region. (`t2.nano` is default vm type on auto-generated stack template
which can be manually changed)
![Stack Building Success](help/integration/img/koding_build-success.png)
You can check out the `Build Logs` from this success modal as well;
![Stack Build Logs](help/integration/img/koding_build-logs.png)
You can now `Start Coding`!
![Edit On IDE](help/integration/img/koding_edit-on-ide.png)
# Try a Merge Request on IDE
It's also possible to try a change on IDE before merging it. This flow only
enabled if the target project has `.koding.yml` in it's target branch. You
should see the alternative version of `Run in IDE (Koding)` button in merge
request pages as well;
![Run in IDE on MR](help/integration/img/koding_run-mr-in-ide.png)
This will again take you to Koding with proper arguments passed, which will
allow Koding to modify the stack template provided by target branch. You can
see the difference;
![Different Branch for MR](help/integration/img/koding_different-stack-on-mr-try.png)
The flow for the branch stack is also same with the regular project flow.
# Open GitLab from Koding
Since stacks generated with import flow defined in previous steps, they have
information about the repository they are belonging to. By using this
information you can access to related GitLab page from stacks on your sidebar
on Koding.
![Open GitLab from Koding](help/integration/img/koding_open-gitlab-from-koding.png)
# Koding & GitLab
This document will guide you through installing and configuring Koding with
GitLab.
First of all, to be able to use Koding and GitLab together you will need public
access to your server. This allows you to use single sign-on from GitLab to
Koding and using vms from cloud providers like AWS. Koding has a registry for
VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
cloud providers register themselves to Kontrol via the agent that we put into
provisioned VMs. This agent is called Klient and it provides Koding to access
and manage the target machine.
Kontrol and Klient are based on another technology called
[Kite](github.com/koding/kite), that we have written at Koding. Which is a
microservice framework that allows you to develop microservices easily.
## Requirements
### Hardware
Minimum requirements are;
- 2 cores CPU
- 3G RAM
- 10G Storage
If you plan to use AWS to install Koding it is recommended that you use at
least a `c3.xlarge` instance.
### Software
- [git](https://git-scm.com)
- [docker](https://www.docker.com)
- [docker-compose](https://www.docker.com/products/docker-compose)
Koding can run on most of the UNIX based operating systems, since it's shipped
as containerized with Docker support, it can work on any operating system that
supports Docker.
Required services are;
- PostgreSQL # Kontrol and Service DB provider
- MongoDB # Main DB provider the application
- Redis # In memory DB used by both application and services
- RabbitMQ # Message Queue for both application and services
which are also provided as a Docker container by Koding.
## Getting Started with Development Versions
### Koding
You can run `docker-compose` environment for developing koding by
executing commands in the following snippet.
```bash
git clone https://github.com/koding/koding.git
cd koding
docker-compose up
```
This should start koding on `localhost:8090`.
By default there is no team exists in Koding DB. You'll need to create a team
called `gitlab` which is the default team name for GitLab integration in the
configuration. To make things in order it's recommended to create the `gitlab`
team first thing after setting up Koding.
### GitLab
To install GitLab to your environment for development purposes it's recommended
to use GitLab Development Kit which you can get it from
[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
After all those steps, gitlab should be running on `localhost:3000`
## Integration
Integration includes following components;
- Single Sign On with OAuth from GitLab to Koding
- System Hook integration for handling GitLab events on Koding
(`project_created`, `user_joined` etc.)
- Service endpoints for importing/executing stacks from GitLab to Koding
(`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
As it's pointed out before, you will need public access to this machine that
you've installed Koding and GitLab on. Better to use a domain but a static IP
is also fine.
For IP based installation you can use [xip.io](https://xip.io) service which is
free and provides DNS resolution to IP based requests like following;
- 127.0.0.1.xip.io -> resolves to 127.0.0.1
- foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
- and so on...
As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
a running koding instance on `127.0.0.1` server will be handled as `foo` team
requests.
### GitLab Side
You need to enable Koding integration from Settings under Admin Area. To do
that login with an Admin account and do followings;
- open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
- scroll to bottom of the page until Koding section
- check `Enable Koding` checkbox
- provide GitLab team page for running Koding instance as `Koding URL`*
* For `Koding URL` you need to provide the gitlab integration enabled team on
your Koding installation. Team called `gitlab` has integration on Koding out
of the box, so if you didn't change anything your team on Koding should be
`gitlab`.
So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
with your Koding installation here.
#### Registering Koding for OAuth integration
We need `Application ID` and `Secret` to enable login to Koding via GitLab
feature and to do that you need to register running Koding as a new application
to your running GitLab application. Follow
[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
enable this integration.
Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
which again you need to _replace `127.0.0.1` with your instance public IP._
Take a copy of `Application ID` and `Secret` that is generated by the GitLab
application, we will need those on _Koding Part_ of this guide.
#### Registering system hooks to Koding (optional)
Koding can take actions based on the events generated by GitLab application.
This feature is still in progress and only following events are processed by
Koding at the moment;
- user_create
- user_destroy
All system events are handled but not implemented on Koding side.
To enable this feature you need to provide a `URL` and a `Secret Token` to your
GitLab application. Open your admin area on your GitLab app from
[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
keep a copy of it, we will need it on _Koding Part_ of this guide.
_(replace `127.0.0.1` with your instance public IP)_
### Koding Part
If you followed the steps in GitLab part we should have followings to enable
Koding part integrations;
- `Application ID` and `Secret` for OAuth integration
- `Secret Token` for system hook integration
- Public address of running GitLab instance
#### Start Koding with GitLab URL
Now we need to configure Koding with all this information to get things ready.
If it's already running please stop koding first.
##### From command-line
Replace followings with the ones you got from GitLab part of this guide;
```bash
cd koding
docker-compose run \
--service-ports backend \
/opt/koding/scripts/bootstrap-container build \
--host=**YOUR_IP**.xip.io \
--gitlabHost=**GITLAB_IP** \
--gitlabPort=**GITLAB_PORT** \
--gitlabToken=**SECRET_TOKEN** \
--gitlabAppId=**APPLICATION_ID** \
--gitlabAppSecret=**SECRET**
```
##### By updating configuration
Alternatively you can update `gitlab` section on
`config/credentials.default.coffee` like following;
```
gitlab =
host: '**GITLAB_IP**'
port: '**GITLAB_PORT**'
applicationId: '**APPLICATION_ID**'
applicationSecret: '**SECRET**'
team: 'gitlab'
redirectUri: ''
systemHookToken: '**SECRET_TOKEN**'
hooksEnabled: yes
```
and start by only providing the `host`;
```bash
cd koding
docker-compose run \
--service-ports backend \
/opt/koding/scripts/bootstrap-container build \
--host=**YOUR_IP**.xip.io \
```
#### Enable Single Sign On
Once you restarted your Koding and logged in with your username and password
you need to activate oauth authentication for your user. To do that
- Navigate to Dashboard on Koding from;
`http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
- Scroll down to Integrations section
- Click on toggle to turn On integration in GitLab integration section
This will redirect you to your GitLab instance and will ask your permission (
if you are not logged in to GitLab at this point you will be redirected after
login) once you accept you will be redirected to your Koding instance.
From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
screen in your Koding instance.
......@@ -761,6 +761,174 @@ X-Gitlab-Event: Wiki Page Hook
}
```
## Pipeline events
Triggered on status change of Pipeline.
**Request Header**:
```
X-Gitlab-Event: Pipeline Hook
```
**Request Body**:
```json
{
"object_kind": "pipeline",
"object_attributes":{
"id": 31,
"ref": "master",
"tag": false,
"sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
"before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
"status": "success",
"stages":[
"build",
"test",
"deploy"
],
"created_at": "2016-08-12 15:23:28 UTC",
"finished_at": "2016-08-12 15:26:29 UTC",
"duration": 63
},
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"project":{
"name": "Gitlab Test",
"description": "Atque in sunt eos similique dolores voluptatem.",
"web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
"avatar_url": null,
"git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
"git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
"namespace": "Gitlab Org",
"visibility_level": 20,
"path_with_namespace": "gitlab-org/gitlab-test",
"default_branch": "master"
},
"commit":{
"id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
"message": "test\n",
"timestamp": "2016-08-12T17:23:21+02:00",
"url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
"author":{
"name": "User",
"email": "user@gitlab.com"
}
},
"builds":[
{
"id": 380,
"stage": "deploy",
"name": "production",
"status": "skipped",
"created_at": "2016-08-12 15:23:28 UTC",
"started_at": null,
"finished_at": null,
"when": "manual",
"manual": true,
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"runner": null,
"artifacts_file":{
"filename": null,
"size": null
}
},
{
"id": 377,
"stage": "test",
"name": "test-image",
"status": "success",
"created_at": "2016-08-12 15:23:28 UTC",
"started_at": "2016-08-12 15:26:12 UTC",
"finished_at": null,
"when": "on_success",
"manual": false,
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"runner": null,
"artifacts_file":{
"filename": null,
"size": null
}
},
{
"id": 378,
"stage": "test",
"name": "test-build",
"status": "success",
"created_at": "2016-08-12 15:23:28 UTC",
"started_at": "2016-08-12 15:26:12 UTC",
"finished_at": "2016-08-12 15:26:29 UTC",
"when": "on_success",
"manual": false,
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"runner": null,
"artifacts_file":{
"filename": null,
"size": null
}
},
{
"id": 376,
"stage": "build",
"name": "build-image",
"status": "success",
"created_at": "2016-08-12 15:23:28 UTC",
"started_at": "2016-08-12 15:24:56 UTC",
"finished_at": "2016-08-12 15:25:26 UTC",
"when": "on_success",
"manual": false,
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"runner": null,
"artifacts_file":{
"filename": null,
"size": null
}
},
{
"id": 379,
"stage": "deploy",
"name": "staging",
"status": "created",
"created_at": "2016-08-12 15:23:28 UTC",
"started_at": null,
"finished_at": null,
"when": "on_success",
"manual": false,
"user":{
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
"runner": null,
"artifacts_file":{
"filename": null,
"size": null
}
}
]
}
```
#### Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
......
......@@ -27,7 +27,6 @@
- [Two-factor Authentication (2FA)](two_factor_authentication.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
- [Issuable Templates](issuable_templates.md)
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md)
......
# Description templates
Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
![Description templates](img/description_templates.png)
_Example:_
`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
# Share Projects with other Groups
In GitLab Enterprise Edition you can share projects with other groups.
This makes it possible to add a group of users to a project with a single action.
You can share projects with other groups. This makes it possible to add a group of users
to a project with a single action.
## Groups as collections of users
In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
Groups are used primarily to [create collections of projects](groups.md), but you can also
take advantage of the fact that groups define collections of _users_, namely the group
members.
## Sharing a project with a group of users
The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the (Enterprise Edition only) group sharing feature can be of use.
The primary mechanism to give a group of users, say 'Engineering', access to a project,
say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
......
......@@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
click_button "Edit access level"
select 'Developer', from: 'group_member_access_level'
click_button 'Edit'
select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save'
end
end
......
......@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content with new lines preserved at end of file' do
expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
end
step 'I click link "Raw"' do
......@@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I can edit code' do
set_new_content
expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
end
step 'I edit code' do
......@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I edit code with new lines at end of file' do
execute_script('blob.editor.setValue("Sample\n\n\n")')
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
end
step 'I fill the new file name' do
......@@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
execute_script("blob.editor.setValue('#{new_gitignore_content}')")
execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
# Content of the gitignore file on the seed repository.
......
......@@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
click_button "Edit access level"
select "Reporter", from: "project_member_access_level"
click_button 'Edit'
select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end
end
......
......@@ -48,7 +48,8 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
expose :issues_events, :merge_requests_events, :tag_push_events
expose :note_events, :build_events, :pipeline_events
expose :enable_ssl_verification
end
......@@ -102,6 +103,10 @@ module API
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.access_level
end
expose :expires_at do |user, options|
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.expires_at
end
end
class AccessRequester < UserBasic
......@@ -378,7 +383,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
expose :push_events, :issues_events, :merge_requests_events
expose :tag_push_events, :note_events, :build_events, :pipeline_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
......
......@@ -49,6 +49,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the new member
# access_level (required) - A valid access level
# expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# POST /groups/:id/members
......@@ -72,7 +73,7 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
source.add_user(params[:user_id], params[:access_level], current_user)
source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
end
......@@ -81,7 +82,7 @@ module API
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
member = source.members.build(attributes_for_keys([:user_id, :access_level]))
member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
......@@ -97,6 +98,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
# access_level (required) - A valid access level
# expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# PUT /groups/:id/members/:user_id
......@@ -107,8 +109,9 @@ module API
required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
attrs = attributes_for_keys [:access_level, :expires_at]
if member.update_attributes(access_level: params[:access_level])
if member.update_attributes(attrs)
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
......
......@@ -45,6 +45,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
:pipeline_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
......@@ -78,6 +79,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
:pipeline_events,
:enable_ssl_verification
]
......
......@@ -94,7 +94,9 @@ module ExtractsPath
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
@id = Addressable::URI.normalize_component(get_id)
@id = params[:id] || params[:ref]
@id += "/" + params[:path] unless params[:path].blank?
@ref, @path = extract_ref(@id)
@repo = @project.repository
if @options[:extended_sha1].blank?
......@@ -116,12 +118,4 @@ module ExtractsPath
def tree
@tree ||= @repo.tree(@commit.id, @path)
end
private
def get_id
id = params[:id] || params[:ref]
id += "/" + params[:path] unless params[:path].blank?
id
end
end
......@@ -13,8 +13,7 @@ module Gitlab
@job = job
@pipeline = @project.pipelines
.where(ref: @ref)
.where(sha: @project.commit(@ref).try(:sha))
.latest_successful_for(@ref)
.first
end
......
......@@ -30,6 +30,7 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
koding_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
......
module Gitlab
class BuildDataBuilder
class << self
module DataBuilder
module Build
extend self
def build(build)
project = build.project
commit = build.pipeline
......
module Gitlab
class NoteDataBuilder
class << self
module DataBuilder
module Note
extend self
# Produce a hash of post-receive data
#
# For all notes:
......
module Gitlab
module DataBuilder
module Pipeline
extend self
def build(pipeline)
{
object_kind: 'pipeline',
object_attributes: hook_attrs(pipeline),
user: pipeline.user.try(:hook_attrs),
project: pipeline.project.hook_attrs(backward: false),
commit: pipeline.commit.try(:hook_attrs),
builds: pipeline.builds.map(&method(:build_hook_attrs))
}
end
def hook_attrs(pipeline)
{
id: pipeline.id,
ref: pipeline.ref,
tag: pipeline.tag,
sha: pipeline.sha,
before_sha: pipeline.before_sha,
status: pipeline.status,
stages: pipeline.stages,
created_at: pipeline.created_at,
finished_at: pipeline.finished_at,
duration: pipeline.duration
}
end
def build_hook_attrs(build)
{
id: build.id,
stage: build.stage,
name: build.name,
status: build.status,
created_at: build.created_at,
started_at: build.started_at,
finished_at: build.finished_at,
when: build.when,
manual: build.manual?,
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
filename: build.artifacts_file.filename,
size: build.artifacts_size
}
}
end
def runner_hook_attrs(runner)
{
id: runner.id,
description: runner.description,
active: runner.active?,
is_shared: runner.is_shared?
}
end
end
end
end
module Gitlab
class PushDataBuilder
class << self
module DataBuilder
module Push
extend self
# Produce a hash of post-receive data
#
# data = {
......
......@@ -139,13 +139,19 @@ module Gitlab
private
def find_diff_file(repository)
diffs = Gitlab::Git::Compare.new(
repository.raw_repository,
start_sha,
head_sha
).diffs(paths: paths)
# We're at the initial commit, so just get that as we can't compare to anything.
if Gitlab::Git.blank_ref?(start_sha)
compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
else
compare = Gitlab::Git::Compare.new(
repository.raw_repository,
start_sha,
head_sha
)
end
diff = compare.diffs(paths: paths).first
diff = diffs.first
return unless diff
Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
......
......@@ -7,7 +7,7 @@ module Gitlab
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
Popen::popen(cmd).last.zero?
Popen.popen(cmd).last.zero?
end
def force_utf8(str)
......
......@@ -5,5 +5,15 @@ FactoryGirl.define do
trait :token do
token { SecureRandom.hex(10) }
end
trait :all_events_enabled do
push_events true
merge_requests_events true
tag_push_events true
issues_events true
note_events true
build_events true
pipeline_events true
end
end
end
......@@ -572,6 +572,18 @@ describe 'Issue Boards', feature: true, js: true do
end
end
context 'keyboard shortcuts' do
before do
visit namespace_project_board_path(project.namespace, project)
wait_for_vue_resource
end
it 'allows user to use keyboard shortcuts' do
find('.boards-list').native.send_keys('i')
expect(page).to have_content('New Issue')
end
end
context 'signed out user' do
before do
logout
......
......@@ -4,12 +4,6 @@ feature 'test coverage badge' do
given!(:user) { create(:user) }
given!(:project) { create(:project, :private) }
given!(:pipeline) do
create(:ci_pipeline, project: project,
ref: 'master',
sha: project.commit.id)
end
context 'when user has access to view badge' do
background do
project.team << [user, :developer]
......@@ -17,8 +11,10 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge image for pipeline' do
create_job(coverage: 100, name: 'test:1')
create_job(coverage: 90, name: 'test:2')
create_pipeline do |pipeline|
create_build(pipeline, coverage: 100, name: 'test:1')
create_build(pipeline, coverage: 90, name: 'test:2')
end
show_test_coverage_badge
......@@ -26,9 +22,11 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for specific job' do
create_job(coverage: 50, name: 'test:1')
create_job(coverage: 50, name: 'test:2')
create_job(coverage: 85, name: 'coverage')
create_pipeline do |pipeline|
create_build(pipeline, coverage: 50, name: 'test:1')
create_build(pipeline, coverage: 50, name: 'test:2')
create_build(pipeline, coverage: 85, name: 'coverage')
end
show_test_coverage_badge(job: 'coverage')
......@@ -36,7 +34,9 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for pipeline without coverage' do
create_job(coverage: nil, name: 'test')
create_pipeline do |pipeline|
create_build(pipeline, coverage: nil, name: 'test')
end
show_test_coverage_badge
......@@ -54,10 +54,19 @@ feature 'test coverage badge' do
end
end
def create_job(coverage:, name:)
create(:ci_build, name: name,
coverage: coverage,
pipeline: pipeline)
def create_pipeline
opts = { project: project, ref: 'master', sha: project.commit.id }
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
pipeline.build_updated
end
end
def create_build(pipeline, coverage:, name:)
opts = { pipeline: pipeline, coverage: coverage, name: name }
create(:ci_build, :success, opts)
end
def show_test_coverage_badge(job: nil)
......
require 'spec_helper'
feature 'Project group links', feature: true, js: true do
include Select2Helper
let(:master) { create(:user) }
let(:project) { create(:project) }
let!(:group) { create(:group) }
background do
project.team << [master, :master]
login_as(master)
end
context 'setting an expiration date for a group link' do
before do
visit namespace_project_group_links_path(project.namespace, project)
select2 group.id, from: '#link_group_id'
fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
click_on 'Share'
end
it 'shows the expiration time with a warning class' do
page.within('.enabled-groups') do
expect(page).to have_content('expires in 4 days')
expect(page).to have_selector('.text-warning')
end
end
end
end
require 'spec_helper'
feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
include Select2Helper
include ActiveSupport::Testing::TimeHelpers
let(:master) { create(:user) }
let(:project) { create(:project) }
let!(:new_member) { create(:user) }
background do
project.team << [master, :master]
login_as(master)
end
scenario 'expiration date is displayed in the members list' do
travel_to Time.zone.parse('2016-08-06 08:00') do
visit namespace_project_project_members_path(project.namespace, project)
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10'
click_on 'Add users to project'
end
page.within '.project_member:first-child' do
expect(page).to have_content('Expires in 4 days')
end
end
end
scenario 'change expiration date' do
travel_to Time.zone.parse('2016-08-06 08:00') do
project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
visit namespace_project_project_members_path(project.namespace, project)
page.within '.project_member:first-child' do
click_on 'Edit'
fill_in 'Access expiration date', with: '2016-08-09'
click_on 'Save'
expect(page).to have_content('Expires in 3 days')
end
end
end
end
......@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
describe "GET /koding" do
subject { koding_path }
context 'with Koding enabled' do
before do
stub_application_setting(koding_enabled?: true)
end
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for :user }
it { is_expected.to be_denied_for :visitor }
end
end
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
......
......@@ -10,23 +10,31 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"labels": {
"type": ["array"],
"required": [
"id",
"color",
"description",
"title",
"priority"
],
"properties": {
"id": { "type": "integer" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"color",
"description",
"title",
"priority"
],
"properties": {
"id": { "type": "integer" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
"description": { "type": ["string", "null"] },
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
},
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
"additionalProperties": false
}
},
"assignee": {
......
require 'spec_helper'
describe IssuablesHelper do
let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) }
context 'label tooltip' do
it 'returns label text' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
it 'returns label text' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
end
......@@ -38,6 +38,11 @@ describe NotesHelper do
end
describe '#preload_max_access_for_authors' do
before do
# This method reads cache from RequestStore, so make sure it's clean.
RequestStore.clear!
end
it 'loads multiple users' do
expected_access = {
owner.id => Gitlab::Access::OWNER,
......
......@@ -19,16 +19,16 @@ describe TimeHelper do
describe "#duration_in_numbers" do
it "returns minutes and seconds" do
duration_in_numbers = {
[100, 0] => "01:40",
[121, 0] => "02:01",
[3721, 0] => "01:02:01",
[0, 0] => "00:00",
[nil, Time.now.to_i - 42] => "00:42"
durations_and_expectations = {
100 => "01:40",
121 => "02:01",
3721 => "01:02:01",
0 => "00:00",
42 => "00:42"
}
duration_in_numbers.each do |interval, expectation|
expect(duration_in_numbers(*interval)).to eq(expectation)
durations_and_expectations.each do |duration, expectation|
expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
......
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip
.title.hide-collapsed
%a.edit-link.pull-right{ href: "#" }
Edit
.selectbox.hide-collapsed{ style: "display: none;" }
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
%span.dropdown-toggle-text
Label
%i.fa.fa-chevron-down
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
.dropdown-content
.dropdown-loading
%i.fa.fa-spinner.fa-spin
//= require lib/utils/type_utility
//= require jquery
//= require bootstrap
//= require gl_dropdown
//= require select2
//= require jquery.nicescroll
//= require api
//= require create_label
//= require issuable_context
//= require users_select
//= require labels_select
(() => {
let saveLabelCount = 0;
describe('Issue dropdown sidebar', () => {
fixture.preload('issue_sidebar_label.html');
beforeEach(() => {
fixture.load('issue_sidebar_label.html');
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => {
const d = $.Deferred();
let LABELS_DATA = []
if (req.url === '/root/test/labels.json') {
for (let i = 0; i < 10; i++) {
LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
}
} else if (req.url === '/root/test/issues/2.json') {
let tmp = []
for (let i = 0; i < saveLabelCount; i++) {
tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
}
LABELS_DATA = {labels: tmp};
}
d.resolve(LABELS_DATA);
return d.promise();
});
});
it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5;
$('.edit-link').get(0).click();
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => {
if (i < 5) {
$link.get(0).click();
}
});
$('.edit-link').get(0).click();
setTimeout(() => {
expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
done();
}, 0);
}, 0);
});
it('changes collapsed tooltip when changing labels when more than 5', (done) => {
saveLabelCount = 6;
$('.edit-link').get(0).click();
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => {
if (i < 5) {
$link.get(0).click();
}
});
$('.edit-link').get(0).click();
setTimeout(() => {
expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
done();
}, 0);
}, 0);
});
});
})();
......@@ -30,17 +30,6 @@ describe ExtractsPath, lib: true do
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
context 'escaped slash character in ref' do
let(:ref) { 'improve%2Fawesome' }
it 'has no escape sequences in @ref or @logs_path' do
assign_ref_vars
expect(@ref).to eq('improve/awesome')
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
end
context 'ref contains %20' do
let(:ref) { 'foo%20bar' }
......@@ -52,6 +41,16 @@ describe ExtractsPath, lib: true do
expect(@id).to start_with('foo%20bar/')
end
end
context 'path contains space' do
let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
it 'is not converted to %20 in @path' do
assign_ref_vars
expect(@path).to eq(params[:path])
end
end
end
describe '#extract_ref' do
......
......@@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do
end
end
context 'pipeline exists' do
let!(:pipeline) do
create(:ci_pipeline, project: project,
sha: project.commit.id,
ref: 'master')
end
context 'when latest successful pipeline exists' do
before do
create_pipeline do |pipeline|
create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
create(:ci_build, :success, pipeline: pipeline, coverage: 60)
end
context 'builds exist' do
before do
create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40)
create(:ci_build, pipeline: pipeline, coverage: 60)
create_pipeline do |pipeline|
create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
end
context 'particular job specified' do
let(:job_name) { 'first' }
context 'when particular job specified' do
let(:job_name) { 'first' }
it 'returns coverage for the particular job' do
expect(badge.status).to eq 40
end
it 'returns coverage for the particular job' do
expect(badge.status).to eq 40
end
end
context 'particular job not specified' do
let(:job_name) { '' }
context 'when particular job not specified' do
let(:job_name) { '' }
it 'returns arithemetic mean for the pipeline' do
expect(badge.status).to eq 50
end
end
end
it 'returns arithemetic mean for the pipeline' do
expect(badge.status).to eq 50
end
context 'when only failed pipeline exists' do
before do
create_pipeline do |pipeline|
create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
end
context 'builds do not exist' do
it_behaves_like 'unknown coverage report'
it_behaves_like 'unknown coverage report'
context 'particular job specified' do
let(:job_name) { 'nonexistent' }
context 'particular job specified' do
let(:job_name) { 'nonexistent' }
it 'retruns nil' do
expect(badge.status).to be_nil
end
it 'retruns nil' do
expect(badge.status).to be_nil
end
end
end
......@@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do
context 'pipeline does not exist' do
it_behaves_like 'unknown coverage report'
end
def create_pipeline
opts = { project: project, sha: project.commit.id, ref: 'master' }
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
pipeline.build_updated
end
end
end
require 'spec_helper'
describe 'Gitlab::BuildDataBuilder' do
describe Gitlab::DataBuilder::Build do
let(:build) { create(:ci_build) }
describe '.build' do
let(:data) do
Gitlab::BuildDataBuilder.build(build)
described_class.build(build)
end
it { expect(data).to be_a(Hash) }
......
require 'spec_helper'
describe 'Gitlab::NoteDataBuilder', lib: true do
describe Gitlab::DataBuilder::Note, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
let(:data) { described_class.build(note, user) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do
......
require 'spec_helper'
describe Gitlab::DataBuilder::Pipeline do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
status: 'success',
sha: project.commit.sha,
ref: project.default_branch)
end
let!(:build) { create(:ci_build, pipeline: pipeline) }
describe '.build' do
let(:data) { described_class.build(pipeline) }
let(:attributes) { data[:object_attributes] }
let(:build_data) { data[:builds].first }
let(:project_data) { data[:project] }
it { expect(attributes).to be_a(Hash) }
it { expect(attributes[:ref]).to eq(pipeline.ref) }
it { expect(attributes[:sha]).to eq(pipeline.sha) }
it { expect(attributes[:tag]).to eq(pipeline.tag) }
it { expect(attributes[:id]).to eq(pipeline.id) }
it { expect(attributes[:status]).to eq(pipeline.status) }
it { expect(build_data).to be_a(Hash) }
it { expect(build_data[:id]).to eq(build.id) }
it { expect(build_data[:status]).to eq(build.status) }
it { expect(project_data).to eq(project.hook_attrs(backward: false)) }
end
end
require 'spec_helper'
describe Gitlab::PushDataBuilder, lib: true do
describe Gitlab::DataBuilder::Push, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
......
......@@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do
end
end
describe "position for a file in the initial commit" do
let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
subject do
described_class.new(
old_path: "README.md",
new_path: "README.md",
old_line: nil,
new_line: 1,
diff_refs: commit.diff_refs
)
end
describe "#diff_file" do
it "returns the correct diff file" do
diff_file = subject.diff_file(project.repository)
expect(diff_file.new_file).to be true
expect(diff_file.new_path).to eq(subject.new_path)
expect(diff_file.diff_refs).to eq(subject.diff_refs)
end
end
describe "#diff_line" do
it "returns the correct diff line" do
diff_line = subject.diff_line(project.repository)
expect(diff_line.added?).to be true
expect(diff_line.new_line).to eq(subject.new_line)
expect(diff_line.text).to eq("+testme")
end
end
describe "#line_code" do
it "returns the correct line code" do
line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
expect(subject.line_code(project.repository)).to eq(line_code)
end
end
end
describe "#to_json" do
let(:hash) do
{
......
......@@ -552,7 +552,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
Member.add_user(
project.project_members,
'toto@example.com',
Gitlab::Access::DEVELOPER,
current_user: inviter
)
project.project_members.invite.last
end
......@@ -799,7 +804,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
Member.add_user(
group.group_members,
'toto@example.com',
Gitlab::Access::DEVELOPER,
current_user: inviter
)
group.group_members.invite.last
end
......
require 'spec_helper'
describe BroadcastMessage, models: true do
include ActiveSupport::Testing::TimeHelpers
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
......
......@@ -42,7 +42,7 @@ describe Ci::Build, models: true do
describe '#ignored?' do
subject { build.ignored? }
context 'if build is not allowed to fail' do
context 'when build is not allowed to fail' do
before do
build.allow_failure = false
end
......@@ -64,7 +64,7 @@ describe Ci::Build, models: true do
end
end
context 'if build is allowed to fail' do
context 'when build is allowed to fail' do
before do
build.allow_failure = true
end
......@@ -92,7 +92,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_empty }
context 'if build.trace contains text' do
context 'when build.trace contains text' do
let(:text) { 'example output' }
before do
build.trace = text
......@@ -102,7 +102,7 @@ describe Ci::Build, models: true do
it { expect(subject.length).to be >= text.length }
end
context 'if build.trace hides token' do
context 'when build.trace hides token' do
let(:token) { 'my_secret_token' }
before do
......@@ -283,13 +283,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
context 'if config is not found' do
context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq(predefined_variables) }
end
context 'if config does not have a questioned job' do
context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
......@@ -301,7 +301,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables) }
end
context 'if config has variables' do
context 'when config has variables' do
let(:config) do
YAML.dump({
test: {
......@@ -393,7 +393,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_falsey }
end
context 'if there are runner' do
context 'when there are runners' do
let(:runner) { create(:ci_runner) }
before do
......@@ -423,29 +423,27 @@ describe Ci::Build, models: true do
describe '#stuck?' do
subject { build.stuck? }
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
before do
build.status = state
end
it { is_expected.to be_truthy }
context "when commit_status.status is pending" do
before do
build.status = 'pending'
end
context "and there are specific runner" do
let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
it { is_expected.to be_truthy }
before do
build.project.runners << runner
runner.save
end
context "and there are specific runner" do
let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
it { is_expected.to be_falsey }
before do
build.project.runners << runner
runner.save
end
it { is_expected.to be_falsey }
end
end
%w(success failed canceled running).each do |state|
context "if commit_status.status is #{state}" do
%w[success failed canceled running].each do |state|
context "when commit_status.status is #{state}" do
before do
build.status = state
end
......@@ -767,7 +765,7 @@ describe Ci::Build, models: true do
describe '#when' do
subject { build.when }
context 'if is undefined' do
context 'when `when` is undefined' do
before do
build.when = nil
end
......@@ -777,13 +775,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
context 'if config is not found' do
context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq('on_success') }
end
context 'if config does not have a questioned job' do
context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
......@@ -795,7 +793,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq('on_success') }
end
context 'if config has when' do
context 'when config has `when`' do
let(:config) do
YAML.dump({
test: {
......@@ -881,7 +879,7 @@ describe Ci::Build, models: true do
subject { build.play }
it 'enques a build' do
it 'enqueues a build' do
is_expected.to be_pending
is_expected.to eq(build)
end
......@@ -901,7 +899,7 @@ describe Ci::Build, models: true do
describe '#when' do
subject { build.when }
context 'if is undefined' do
context 'when `when` is undefined' do
before do
build.when = nil
end
......@@ -911,13 +909,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
context 'if config is not found' do
context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq('on_success') }
end
context 'if config does not have a questioned job' do
context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
......@@ -929,7 +927,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq('on_success') }
end
context 'if config has when' do
context 'when config has when' do
let(:config) do
YAML.dump({
test: {
......
......@@ -2,7 +2,7 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
let(:project) { FactoryGirl.create :empty_project }
let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
......@@ -18,6 +18,8 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
it { is_expected.to delegate_method(:stages).to(:statuses) }
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
......@@ -122,17 +124,21 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
describe '#duration' do
before do
build.skip
build2.skip
travel_to(current - 120) do
pipeline.run
end
travel_to(current) do
pipeline.succeed
end
end
it 'matches sum of builds duration' do
expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
expect(pipeline.reload.duration).to eq(120)
end
end
......@@ -310,4 +316,87 @@ describe Ci::Pipeline, models: true do
it { is_expected.to eq('running') }
end
end
describe '#execute_hooks' do
let!(:build_a) { create_build('a') }
let!(:build_b) { create_build('b') }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
end
before do
ProjectWebHookWorker.drain
end
context 'with pipeline hooks enabled' do
let(:enabled) { true }
before do
WebMock.stub_request(:post, hook.url)
end
context 'with multiple builds' do
context 'when build is queued' do
before do
build_a.enqueue
build_b.enqueue
end
it 'receive a pending event once' do
expect(WebMock).to have_requested_pipeline_hook('pending').once
end
end
context 'when build is run' do
before do
build_a.enqueue
build_a.run
build_b.enqueue
build_b.run
end
it 'receive a running event once' do
expect(WebMock).to have_requested_pipeline_hook('running').once
end
end
context 'when all builds succeed' do
before do
build_a.success
build_b.success
end
it 'receive a success event once' do
expect(WebMock).to have_requested_pipeline_hook('success').once
end
end
def have_requested_pipeline_hook(status)
have_requested(:post, hook.url).with do |req|
json_body = JSON.parse(req.body)
json_body['object_attributes']['status'] == status &&
json_body['builds'].length == 2
end
end
end
end
context 'with pipeline hooks disabled' do
let(:enabled) { false }
before do
build_a.enqueue
build_b.enqueue
end
it 'did not execute pipeline_hook after touched' do
expect(WebMock).not_to have_requested(:post, hook.url)
end
end
def create_build(name)
create(:ci_build, :created, pipeline: pipeline, name: name)
end
end
end
......@@ -65,11 +65,21 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
Member.add_user(
project.members,
'toto1@example.com',
Gitlab::Access::DEVELOPER,
current_user: @master_user
)
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
Member.add_user(
project.members,
'toto2@example.com',
Gitlab::Access::DEVELOPER,
current_user: @master_user
)
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
......
......@@ -39,7 +39,7 @@ describe AssemblaService, models: true do
token: 'verySecret',
subdomain: 'project_name'
)
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
WebMock.stub_request(:post, @api_url)
end
......
require 'spec_helper'
describe BuildsEmailService do
let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) }
let(:data) do
Gitlab::DataBuilder::Build.build(create(:ci_build))
end
describe 'Validations' do
context 'when service is active' do
......@@ -39,7 +41,7 @@ describe BuildsEmailService do
describe '#test' do
it 'sends email' do
data = Gitlab::BuildDataBuilder.build(create(:ci_build))
data = Gitlab::DataBuilder::Build.build(create(:ci_build))
subject.recipients = 'test@gitlab.com'
expect(BuildEmailWorker).to receive(:perform_async)
......@@ -49,7 +51,7 @@ describe BuildsEmailService do
context 'notify only failed builds is true' do
it 'sends email' do
data = Gitlab::BuildDataBuilder.build(create(:ci_build))
data = Gitlab::DataBuilder::Build.build(create(:ci_build))
data[:build_status] = "success"
subject.recipients = 'test@gitlab.com'
......
......@@ -54,7 +54,7 @@ describe CampfireService, models: true do
subdomain: 'project-name',
room: 'test-room'
)
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
@headers = { 'Content-Type' => 'application/json; charset=utf-8' }
end
......
......@@ -84,7 +84,9 @@ describe DroneCiService, models: true do
include_context :drone_ci_service
let(:user) { create(:user, username: 'username') }
let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
it do
service_hook = double
......
......@@ -52,7 +52,7 @@ describe FlowdockService, models: true do
service_hook: true,
token: 'verySecret'
)
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://api.flowdock.com/v1/messages'
WebMock.stub_request(:post, @api_url)
end
......
......@@ -55,7 +55,7 @@ describe GemnasiumService, models: true do
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
......
......@@ -48,7 +48,9 @@ describe HipchatService, models: true do
let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
before(:each) do
allow(hipchat).to receive_messages(
......@@ -108,7 +110,15 @@ describe HipchatService, models: true do
end
context 'tag_push events' do
let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build(
project,
user,
Gitlab::Git::BLANK_SHA,
'1' * 40,
'refs/tags/test',
[])
end
it "calls Hipchat API for tag push events" do
hipchat.execute(push_sample_data)
......@@ -198,7 +208,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for commit comment events" do
data = Gitlab::NoteDataBuilder.build(commit_note, user)
data = Gitlab::DataBuilder::Note.build(commit_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
......@@ -230,7 +240,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for merge request comment events" do
data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
......@@ -257,7 +267,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for issue comment events" do
data = Gitlab::NoteDataBuilder.build(issue_note, user)
data = Gitlab::DataBuilder::Note.build(issue_note, user)
hipchat.execute(data)
message = hipchat.send(:create_message, data)
......@@ -283,7 +293,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for snippet comment events" do
data = Gitlab::NoteDataBuilder.build(snippet_note, user)
data = Gitlab::DataBuilder::Note.build(snippet_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
......@@ -306,7 +316,7 @@ describe HipchatService, models: true do
context 'build events' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
let(:data) { Gitlab::DataBuilder::Build.build(build) }
context 'for failed' do
before { build.drop }
......
......@@ -46,7 +46,9 @@ describe IrkerService, models: true do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
let(:colorize_messages) { '1' }
......
......@@ -66,7 +66,7 @@ describe JiraService, models: true do
password: 'gitlab_jira_password'
)
@jira_service.save # will build API URL, as api_url was not specified above
@sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
......
......@@ -48,7 +48,9 @@ describe PushoverService, models: true do
let(:pushover) { PushoverService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
let(:api_key) { 'verySecret' }
let(:user_key) { 'verySecret' }
......
......@@ -45,7 +45,9 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
......@@ -195,7 +197,7 @@ describe SlackService, models: true do
it "uses the right channel" do
slack.update_attributes(note_channel: "random")
note_data = Gitlab::NoteDataBuilder.build(issue_note, user)
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
......@@ -235,7 +237,7 @@ describe SlackService, models: true do
end
it "calls Slack API for commit comment events" do
data = Gitlab::NoteDataBuilder.build(commit_note, user)
data = Gitlab::DataBuilder::Note.build(commit_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
......@@ -249,7 +251,7 @@ describe SlackService, models: true do
end
it "calls Slack API for merge request comment events" do
data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
......@@ -262,7 +264,7 @@ describe SlackService, models: true do
end
it "calls Slack API for issue comment events" do
data = Gitlab::NoteDataBuilder.build(issue_note, user)
data = Gitlab::DataBuilder::Note.build(issue_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
......@@ -276,7 +278,7 @@ describe SlackService, models: true do
end
it "calls Slack API for snippet comment events" do
data = Gitlab::NoteDataBuilder.build(snippet_note, user)
data = Gitlab::DataBuilder::Note.build(snippet_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
......
......@@ -719,6 +719,14 @@ describe Repository, models: true do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
merge_commit_id = repository.merge(user, merge_request, commit_options)
repository.commit(merge_commit_id)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
end
describe '#ff_merge' do
......
......@@ -930,7 +930,9 @@ describe User, models: true do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project1) }
let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) }
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
before do
......
......@@ -122,12 +122,13 @@ describe API::Members, api: true do
it 'creates a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: Member::DEVELOPER
user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
expect(response).to have_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
expect(json_response['expires_at']).to eq('2016-08-05')
end
end
......@@ -183,11 +184,12 @@ describe API::Members, api: true do
context 'when authenticated as a master/owner' do
it 'updates the member' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: Member::MASTER
access_level: Member::MASTER, expires_at: '2016-08-05'
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
expect(json_response['expires_at']).to eq('2016-08-05')
end
end
......
......@@ -7,9 +7,9 @@ describe API::API, 'ProjectHooks', api: true do
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
project: project, url: "http://example.com",
push_events: true, merge_requests_events: true, tag_push_events: true,
issues_events: true, note_events: true, build_events: true,
:all_events_enabled,
project: project,
url: 'http://example.com',
enable_ssl_verification: true)
end
......@@ -33,6 +33,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
......@@ -91,6 +92,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
......
......@@ -116,12 +116,19 @@ describe HelpController, "routing" do
expect(get(path)).to route_to('help#show',
path: 'workflow/protected_branches/protected_branches1',
format: 'png')
path = '/help/ui'
expect(get(path)).to route_to('help#ui')
end
end
# koding GET /koding(.:format) koding#index
describe KodingController, "routing" do
it "to #index" do
expect(get("/koding")).to route_to('koding#index')
end
end
# profile_account GET /profile/account(.:format) profile#account
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
......
......@@ -3,8 +3,6 @@ require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do
let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
let(:all_builds) { pipeline.builds }
let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
let(:config) { nil }
before do
......@@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do
end
describe '#execute' do
def all_builds
pipeline.builds
end
def builds
all_builds.where.not(status: [:created, :skipped])
end
def create_builds
described_class.new(pipeline.project, user).execute(pipeline)
end
......@@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do
it 'does not process pipeline if existing stage is running' do
expect(create_builds).to be_truthy
expect(builds.pending.count).to eq(2)
expect(create_builds).to be_falsey
expect(builds.pending.count).to eq(2)
end
......@@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do
end
end
context 'when failed build in the middle stage is retried' do
context 'when failed build is the only unsuccessful build in the stage' do
before do
create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
end
it 'does trigger builds in the next stage' do
expect(create_builds).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
pipeline.builds.running_or_pending.each(&:success)
expect(builds.pluck(:name))
.to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
pipeline.builds.find_by(name: 'test:1').success
pipeline.builds.find_by(name: 'test:2').drop
expect(builds.pluck(:name))
.to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
expect(builds.pluck(:name)).to contain_exactly(
'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
end
end
end
context 'creates a builds from .gitlab-ci.yml' do
let(:config) do
YAML.dump({
......
......@@ -1148,6 +1148,46 @@ describe NotificationService, services: true do
end
end
describe 'GroupMember' do
describe '#decline_group_invite' do
let(:creator) { create(:user) }
let(:group) { create(:group) }
let(:member) { create(:user) }
before(:each) do
group.add_owner(creator)
group.add_developer(member, creator)
end
it do
group_member = group.members.first
expect do
notification.decline_group_invite(group_member)
end.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
end
describe 'ProjectMember' do
describe '#decline_group_invite' do
let(:project) { create(:project) }
let(:member) { create(:user) }
before(:each) do
project.team << [member, :developer, project.owner]
end
it do
project_member = project.members.first
expect do
notification.decline_project_invite(project_member)
end.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
end
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
......
......@@ -34,6 +34,7 @@ RSpec.configure do |config|
config.include EmailHelpers
config.include TestEnv
config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
config.include Rails.application.routes.url_helpers, type: :routing
......
......@@ -5,7 +5,7 @@ describe BuildEmailWorker do
let(:build) { create(:ci_build) }
let(:user) { create(:user) }
let(:data) { Gitlab::BuildDataBuilder.build(build) }
let(:data) { Gitlab::DataBuilder::Build.build(build) }
subject { BuildEmailWorker.new }
......
......@@ -2,19 +2,19 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
context "when push is a new branch" do
let(:email) { ActionMailer::Base.deliveries.last }
before do
data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
......@@ -31,8 +31,6 @@ describe EmailsOnPushWorker do
end
context "when push is a deleted branch" do
let(:email) { ActionMailer::Base.deliveries.last }
before do
data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
......@@ -48,15 +46,40 @@ describe EmailsOnPushWorker do
end
end
context "when there are no errors in sending" do
let(:email) { ActionMailer::Base.deliveries.last }
context "when push is a force push to delete commits" do
before do
data_force_push = data.stringify_keys.merge(
"after" => data[:before],
"before" => data[:after]
)
subject.perform(project.id, recipients, data_force_push)
end
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
it "mentions force pushing in the body" do
expect(email).to have_body_text("force push")
end
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
end
context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
it "does not mention force pushing in the body" do
expect(email).not_to have_body_text("force push")
end
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
......@@ -66,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
allow(subject).to receive_message_chain(:logger, :info)
perform
end
......
require 'spec_helper'
describe RemoveExpiredGroupLinksWorker do
describe '#perform' do
let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
it 'removes expired group links' do
expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
end
it 'leaves group links that expire in the future' do
subject.perform
expect(project_group_link_expiring_in_future.reload).to be_present
end
it 'leaves group links that do not expire at all' do
subject.perform
expect(non_expiring_project_group_link.reload).to be_present
end
end
end
require 'spec_helper'
describe RemoveExpiredMembersWorker do
let(:worker) { RemoveExpiredMembersWorker.new }
describe '#perform' do
context 'project members' do
let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
it 'removes expired members' do
expect { worker.perform }.to change { Member.count }.by(-1)
expect(Member.find_by(id: expired_project_member.id)).to be_nil
end
it 'leaves members that expire in the future' do
worker.perform
expect(project_member_expiring_in_future.reload).to be_present
end
it 'leaves members that do not expire at all' do
worker.perform
expect(non_expiring_project_member.reload).to be_present
end
end
context 'group members' do
let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
it 'removes expired members' do
expect { worker.perform }.to change { Member.count }.by(-1)
expect(Member.find_by(id: expired_group_member.id)).to be_nil
end
it 'leaves members that expire in the future' do
worker.perform
expect(group_member_expiring_in_future.reload).to be_present
end
it 'leaves members that do not expire at all' do
worker.perform
expect(non_expiring_group_member.reload).to be_present
end
end
context 'when the last group owner expires' do
let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
it 'does not delete the owner' do
worker.perform
expect(expired_group_owner.reload).to be_present
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment