Commit 9329436d authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' into dz-merge-request-version

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 6db65143 fb84439a
No related merge requests found
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)
- Add delimiter to project stars and forks count (ClemMakesApps)
- 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
......@@ -18,13 +21,17 @@ 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)
- Fix badge count alignment (ClemMakesApps)
- 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)
- Allow naming U2F devices !5833
- 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)
......@@ -34,9 +41,11 @@ 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)
- Sort todos by date and priority
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
- Send notification emails to users newly mentioned in issue and MR edits !5800
......@@ -55,6 +64,7 @@ v 8.11.0 (unreleased)
- Enforce 2FA restrictions on API authentication endpoints !5820
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
- Fix branch title trailing space on hover (ClemMakesApps)
- Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
......@@ -71,6 +81,7 @@ v 8.11.0 (unreleased)
- Fix devise deprecation warnings.
- Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
- Update version_sorter and use new interface for faster tag sorting
- Load branches asynchronously in Cherry Pick and Revert dialogs.
- Optimize checking if a user has read access to a list of issues !5370
- Store all DB secrets in secrets.yml, under descriptive names !5274
- Fix syntax highlighting in file editor
......@@ -79,7 +90,6 @@ v 8.11.0 (unreleased)
- Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor)
- Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
- Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
- Fix filter input alignment (ClemMakesApps)
- Include old revision in merge request update hooks (Ben Boeckel)
- Add build event color in HipChat messages (David Eisner)
......@@ -105,12 +115,14 @@ v 8.11.0 (unreleased)
- Fix search for notes which belongs to deleted objects
- Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
- Improve OAuth2 client documentation (muteor)
- Fix diff comments inverted toggle bug (ClemMakesApps)
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
- Profile requests when a header is passed
- Fix button missing type (ClemMakesApps)
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
- Add commit stats in commit api. !5517 (dixpac)
......@@ -119,14 +131,17 @@ v 8.11.0 (unreleased)
- edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved
- Move to project dropdown with infinite scroll for better performance
- Fix skip_repo parameter being ignored when destroying a namespace
- Add all builds into stage/job dropdowns on builds page
- 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)
......@@ -141,6 +156,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
......@@ -148,6 +164,10 @@ v 8.11.0 (unreleased)
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
v 8.10.7
- Upgrade Hamlit to 2.6.1. !5873
- Upgrade Doorkeeper to 4.2.0. !5881
v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Restore "Largest repository" sort option on Admin > Projects page. !5797
......@@ -371,6 +391,9 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
v 8.9.8
- Upgrade Doorkeeper to 4.2.0. !5881
v 8.9.7
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
......@@ -640,6 +663,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
v 8.8.9
- Upgrade Doorkeeper to 4.2.0. !5881
v 8.8.8
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
......
......@@ -387,7 +387,8 @@ description area. Copy-paste it to retain the markdown format.
1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
exposing a bug in existing code)
exposing a bug in existing code). Every new class should have corresponding
unit tests, even if the class is exercised at a higher level, such as a feature test.
1. If you suspect a failing CI build is unrelated to your contribution, you may
try and restart the failing CI job or ask a developer to fix the
aforementioned failing test
......
3.3.3
3.4.0
<?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
......@@ -26,8 +26,6 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
/*= require mousetrap */
......@@ -153,7 +151,9 @@
});
});
$('.remove-row').bind('ajax:success', function() {
return $(this).closest('li').fadeOut();
$(this).tooltip('destroy')
.closest('li')
.fadeOut();
});
$('.js-remove-tr').bind('ajax:before', function() {
return $(this).hide();
......
/*= 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;
}
......
File mode changed from 100755 to 100644
......@@ -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':
......
......@@ -31,9 +31,8 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13) {
e.preventDefault()
e.preventDefault();
}
})
.on('keyup', function(e) {
......@@ -111,9 +110,9 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
return $el.show();
return $el.show().removeClass('option-hidden');
} else {
return $el.hide();
return $el.hide().addClass('option-hidden');
}
}
});
......@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
......@@ -191,6 +190,12 @@
currentIndex = -1;
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
CURSOR_SELECT_SCROLL_PADDING = 5
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
......@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
......@@ -240,7 +246,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
selector = '.dropdown-content li:not(.divider)';
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -256,7 +262,7 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
selector = '.dropdown-content li:not(.divider):visible';
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -376,7 +382,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
......@@ -387,7 +393,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
currentIndex = -1;
this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
......@@ -410,6 +416,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
......@@ -463,14 +470,15 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
return "<li class='dropdown-header'>" + data.header + "</li>";
return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
......@@ -494,11 +502,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
url: url,
groupAttrs: groupAttrs,
cssClass: cssClass,
text: text
});
}
return html;
};
......@@ -520,20 +533,8 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
GitLabDropdown.prototype.highlightRow = function(index) {
var selector;
if (this.filterInput.val() !== "") {
selector = '.dropdown-content li:first-child a';
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content li:first-child a";
}
return this.getElement(selector).addClass('is-focused');
}
};
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
......@@ -545,6 +546,7 @@
selectedObject = this.renderedData[selectedIndex];
}
}
fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
......@@ -559,10 +561,9 @@
field.remove();
}
if (this.options.toggleLabel) {
return this.updateLabel(selectedObject, el, this);
} else {
return selectedObject;
this.updateLabel(selectedObject, el, this);
}
return selectedObject;
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
......@@ -570,7 +571,7 @@
field.remove();
}
if (!field.length && fieldName) {
this.addInput(fieldName, value);
this.addInput(fieldName, value, selectedObject);
}
return selectedObject;
} else {
......@@ -589,7 +590,7 @@
}
if (value != null) {
if (!field.length && fieldName) {
this.addInput(fieldName, value);
this.addInput(fieldName, value, selectedObject);
} else {
field.val(value).trigger('change');
}
......@@ -598,24 +599,29 @@
}
};
GitLabDropdown.prototype.addInput = function(fieldName, value) {
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
if (selectedObject && selectedObject.type) {
$input.attr('data-type', selectedObject.type);
}
return this.dropdown.before($input);
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
return $el.first().trigger('click');
$el.first().trigger('click');
var href = $el.attr('href');
if (href && href !== '#') Turbolinks.visit(href);
}
};
......@@ -623,7 +629,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
......@@ -651,7 +657,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
return _this.selectRowAtIndex(currentIndex);
}
};
})(this));
......@@ -661,6 +667,11 @@
return $('body').off('keydown');
};
GitLabDropdown.prototype.resetRows = function resetRows() {
currentIndex = -1;
$('.is-focused', this.dropdown).removeClass('is-focused');
};
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
......@@ -674,10 +685,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
if (!index) {
$dropdownContent.scrollTop(0)
} else if (index === ($listItems.length - 1)) {
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
......
......@@ -102,20 +102,34 @@
};
IssuableForm.prototype.initMoveDropdown = function() {
var $moveDropdown;
var $moveDropdown, pageSize;
$moveDropdown = $('.js-move-dropdown');
if ($moveDropdown.length) {
pageSize = $moveDropdown.data('page-size');
return $('.js-move-dropdown').select2({
ajax: {
url: $moveDropdown.data('projects-url'),
results: function(data) {
quietMillis: 125,
data: function(term, page, context) {
return {
results: data
search: term,
offset_id: context
};
},
data: function(query) {
results: function(data) {
var context,
more;
if (data.length >= pageSize)
more = true;
if (data[data.length - 1])
context = data[data.length - 1].id;
return {
search: query
results: data,
more: more,
context: context
};
}
},
......
......@@ -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);
......@@ -65,7 +65,8 @@
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref')
}
},
dataType: "json"
}).done(function(refs) {
return callback(refs);
});
......@@ -73,7 +74,7 @@
selectable: true,
filterable: true,
filterByText: true,
fieldName: 'ref',
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var link;
if (ref.header != null) {
......
......@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
return ProjectMembers;
})();
}).call(this);
......@@ -10,8 +10,12 @@
selectable: true,
inputId: $dropdown.data('input-id'),
fieldName: $dropdown.data('field-name'),
toggleLabel(item) {
return item.text;
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
} else {
return 'Select';
}
},
clicked(item, $el, e) {
e.preventDefault();
......
......@@ -47,9 +47,7 @@
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
this.$form.find('input[type="submit"]').removeAttr('disabled');
}
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
}
......
......@@ -31,6 +31,9 @@
const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
......
......@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13
ENTER: 13,
UP: 38,
DOWN: 40
};
function SearchAutocomplete(opts) {
......@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
this.disableAutocomplete();
break;
case KEYCODE.UP:
case KEYCODE.DOWN:
return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
......@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open');
return this.restoreMenu();
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
};
SearchAutocomplete.prototype.restoreMenu = function() {
......
/*= require_tree . */
(function() {
$(function() {
var editor = ace.edit("editor")
$(".snippet-form-holder form").on('submit', function() {
$(".snippet-file-content").val(editor.getValue());
});
});
}).call(this);
......@@ -90,6 +90,15 @@
width: 100%;
}
}
// Allows dynamic-width text in the dropdown toggle.
// Resizes to allow long text without overflowing the container.
&.dynamic {
width: auto;
min-width: 160px;
max-width: 100%;
padding-right: 25px;
}
}
.dropdown-menu,
......
......@@ -63,9 +63,10 @@
&.image_file {
background: #eee;
text-align: center;
img {
padding: 100px;
max-width: 50%;
padding: 20px;
max-width: 80%;
}
}
......
......@@ -2,7 +2,7 @@
* Styles that apply to all GFM related forms.
*/
.gfm-commit, .gfm-commit_range {
.gfm-commit_range {
font-family: $monospace_font;
font-size: 90%;
}
.modal-body {
position: relative;
overflow-y: auto;
padding: 15px;
.form-actions {
......
......@@ -72,6 +72,7 @@
font-weight: normal;
background-color: #eee;
color: #78a;
vertical-align: baseline;
}
}
......
......@@ -45,7 +45,8 @@
min-width: 175px;
}
.select2-results .select2-result-label {
.select2-results .select2-result-label,
.select2-more-results {
padding: 10px 15px;
}
......
......@@ -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;
}
}
}
......@@ -384,3 +384,10 @@
color: $gl-dark-link-color;
}
}
.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 {
......
......@@ -719,3 +719,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;
}
}
}
......@@ -109,6 +109,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
......
......@@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
end
......
......@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
@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,
expires_at: params[:expires_at]
)
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
......@@ -63,7 +68,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
......
......@@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
respond_to do |format|
format.html
format.json do
render json: @repository.branch_names
end
end
end
def recent
......
......@@ -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)
......
......@@ -36,7 +36,12 @@ 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
)
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
......@@ -94,7 +99,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
......
class MoveToProjectFinder
PAGE_SIZE = 50
def initialize(user)
@user = user
end
......@@ -8,6 +10,10 @@ class MoveToProjectFinder
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
# infinite scroll using offset
projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
projects = projects.limit(PAGE_SIZE)
# to ask for Project#name_with_namespace
projects.includes(namespace: :owner)
end
......
......@@ -33,7 +33,7 @@ class TodosFinder
# the project IDs yielded by the todos query thus far
items = by_project(items)
items.reorder(id: :desc)
sort(items)
end
private
......@@ -106,6 +106,10 @@ class TodosFinder
params[:type]
end
def sort(items)
params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
end
def by_action(items)
if action?
items = items.where(action: to_action_id)
......
......@@ -31,6 +31,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?
......
......@@ -236,6 +236,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
......@@ -166,38 +166,44 @@ class Ability
end
def project_abilities(user, project)
rules = []
key = "/user/#{user.id}/project/#{project.id}"
RequestStore.store[key] ||= begin
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
if RequestStore.active?
RequestStore.store[key] ||= uncached_project_abilities(user, project)
else
uncached_project_abilities(user, project)
end
end
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
def uncached_project_abilities(user, project)
rules = []
# Push abilities on the users team role
rules.push(*project_team_rules(project.team, user))
if owner
rules.push(*project_owner_rules)
end
owner = user.admin? ||
project.owner == user ||
(project.group && project.group.has_owner?(user))
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
if owner
rules.push(*project_owner_rules)
end
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
if project.public? || (project.internal? && !user.external?)
rules.push(*public_project_rules)
unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access if project.request_access_enabled
end
end
# Allow to read builds for internal projects
rules << :read_build if project.public_builds?
if project.archived?
rules -= project_archived_rules
unless owner || project.team.member?(user) || project_group_member?(project, user)
rules << :request_access if project.request_access_enabled
end
end
rules - project_disabled_features_rules(project)
if project.archived?
rules -= project_archived_rules
end
(rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
......
......@@ -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 }
......@@ -149,6 +153,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
......
......@@ -78,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
......@@ -146,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)
......@@ -250,7 +258,7 @@ module Ci
end
def update_duration
self.duration = statuses.latest.duration
self.duration = calculate_duration
end
def execute_hooks
......
......@@ -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
......@@ -131,7 +131,10 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
condition_field = "#{table_name}.id"
highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
......@@ -159,20 +162,6 @@ module Issuable
grouping_columns
end
private
def highest_label_priority(excluded_labels)
query = Label.select(Label.arel_table[:priority].minimum).
joins(:label_links).
where(label_links: { target_type: name }).
where("label_links.target_id = #{table_name}.id").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?
query
end
end
def today?
......
......@@ -17,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
def original_line_code
raise NotImplementedError
end
def diff_attributes
raise NotImplementedError
end
......
......@@ -35,5 +35,19 @@ module Sortable
all
end
end
private
def highest_label_priority(object_types, condition_field, excluded_labels: [])
query = Label.select(Label.arel_table[:priority].minimum).
joins(:label_links).
where(label_links: { target_type: object_types }).
where("label_links.target_id = #{condition_field}").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?
query
end
end
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
......@@ -16,6 +16,9 @@ class DiffNote < Note
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
# We need to do this again, because it's already in `Note`, but is affected by
# `update_position` and needs to run after that.
before_validation :set_discussion_id
after_save :keep_around_commits
class << self
......@@ -57,6 +60,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?,
......
......@@ -95,34 +95,40 @@ class Group < Namespace
end
end
def add_users(user_ids, access_level, current_user = nil)
def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
Member.add_user(self.group_members, user_id, access_level, current_user)
Member.add_user(
self.group_members,
user_id,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def add_user(user, access_level, current_user = nil)
add_users([user], access_level, current_user)
def add_user(user, access_level, current_user: nil, expires_at: nil)
add_users([user], access_level, current_user: current_user, expires_at: expires_at)
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 add_owner(user, current_user = nil)
add_user(user, Gitlab::Access::OWNER, current_user)
add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
......
......@@ -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
......@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
def add_user(members, user_id, access_level, current_user = nil)
def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
......@@ -87,6 +88,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.save
end
......
......@@ -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
......
......@@ -259,6 +259,8 @@ class Note < ActiveRecord::Base
def ensure_discussion_id
return unless self.persisted?
# Needed in case the SELECT statement doesn't ask for `discussion_id`
return unless self.has_attribute?(:discussion_id)
return if self.discussion_id
set_discussion_id
......
......@@ -611,7 +611,10 @@ class Project < ActiveRecord::Base
end
def new_issue_address(author)
if Gitlab::IncomingEmail.enabled? && author
# This feature is disabled for the time being.
return nil
if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
Gitlab::IncomingEmail.reply_address(
"#{path_with_namespace}+#{author.authentication_token}")
end
......@@ -1003,8 +1006,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
......
......@@ -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,17 +33,18 @@ class ProjectTeam
member
end
def add_users(users, access, current_user = nil)
def add_users(users, access, current_user: nil, expires_at: nil)
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
......
......@@ -277,7 +277,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.
......@@ -553,6 +553,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?
......
class Todo < ActiveRecord::Base
include Sortable
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
......@@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
class << self
def sort(method)
method == "priority" ? order_by_labels_priority : order_by(method)
end
# Order by priority depending on which issue/merge request the Todo belongs to
# Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority
highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
order('todos.created_at')
end
end
def build_failed?
action == BUILD_FAILED
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
......@@ -242,7 +242,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
......@@ -269,7 +268,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
......
......@@ -388,6 +388,25 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
%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
......
......@@ -43,6 +43,25 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Action'})
.pull-right
.dropdown.inline.prepend-left-10
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
= sort_title_recently_created
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
= sort_title_priority
= link_to todos_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
.prepend-top-default
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
......
......@@ -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",
......
......@@ -5,8 +5,8 @@
- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
%span.item-title.str-truncated= branch.name
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
......
- 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")
......
......@@ -17,7 +17,9 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
= select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
= hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
= dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }})
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
......
......@@ -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
......
......@@ -22,7 +22,7 @@
- if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
......
......@@ -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
......
......@@ -14,7 +14,7 @@
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
......
......@@ -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"
- page_title "Members"
.project-members-page.prepend-top-default
.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
......
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
new MemberExpirationDate();
......@@ -22,16 +22,20 @@
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.merge_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.form-group
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
- @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
......@@ -6,7 +6,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title "Switch branch/tag"
= dropdown_filter "Search branches and tags"
......
<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
......@@ -121,7 +121,7 @@
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
= hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
= hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
......
......@@ -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());
});
......@@ -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
......@@ -88,6 +88,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"
......
......@@ -293,6 +293,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
......
......@@ -90,6 +90,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 Gitlab::Seeder::Builds
class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
......@@ -7,11 +7,12 @@ class Gitlab::Seeder::Builds
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :pending },
{ name: 'spinach:osx', stage: 'test', status: :canceled },
{ name: 'cucumber:linux', stage: 'test', status: :running },
{ name: 'cucumber:osx', stage: 'test', status: :failed },
{ name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
{ name: 'spinach:linux', stage: 'test', status: :success },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
{ name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
{ name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
{ name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
{ name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
......@@ -34,72 +35,86 @@ class Gitlab::Seeder::Builds
end
end
private
def pipelines
master_pipelines + merge_request_pipelines
create_master_pipelines + create_merge_request_pipelines
end
def master_pipelines
create_pipelines_for(@project, 'master')
def create_master_pipelines
@project.repository.commits('master', limit: 4).map do |commit|
create_pipeline!(@project, 'master', commit)
end
rescue
[]
end
def merge_request_pipelines
@project.merge_requests.last(5).map do |merge_request|
create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5))
end.flatten
def create_merge_request_pipelines
pipelines = @project.merge_requests.first(3).map do |merge_request|
project = merge_request.source_project
branch = merge_request.source_branch
merge_request.commits.last(4).map do |commit|
create_pipeline!(project, branch, commit)
end
end
pipelines.flatten
rescue
[]
end
def create_pipelines_for(project, ref)
commits = project.repository.commits(ref, limit: 5)
create_pipelines(project, ref, commits)
def create_pipeline!(project, ref, commit)
project.pipelines.create(sha: commit.id, ref: ref)
end
def create_pipelines(project, ref, commits)
commits.map do |commit|
project.pipelines.create(sha: commit.id, ref: ref)
def build_create!(pipeline, opts = {})
attributes = job_attributes(pipeline, opts)
.merge(commands: '$ build command')
Ci::Build.create!(attributes).tap do |build|
# We need to set build trace and artifacts after saving a build
# (id required), that is why we need `#tap` method instead of passing
# block directly to `Ci::Build#create!`.
setup_artifacts(build)
setup_build_log(build)
build.save
end
end
def build_create!(pipeline, opts = {})
attributes = build_attributes_for(pipeline, opts)
def setup_artifacts(build)
return unless %w[build test].include?(build.stage)
Ci::Build.create!(attributes) do |build|
if opts[:name].start_with?('build')
artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file
end
artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file
end
artifacts_cache_file(artifacts_metadata_path) do |file|
build.artifacts_metadata = file
end
end
artifacts_cache_file(artifacts_metadata_path) do |file|
build.artifacts_metadata = file
end
end
if %w(running success failed).include?(build.status)
# We need to set build trace after saving a build (id required)
build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end
def setup_build_log(build)
if %w(running success failed).include?(build.status)
build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end
end
def commit_status_create!(pipeline, opts = {})
attributes = commit_status_attributes_for(pipeline, opts)
attributes = job_attributes(pipeline, opts)
GenericCommitStatus.create!(attributes)
end
def commit_status_attributes_for(pipeline, opts)
def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
def build_attributes_for(pipeline, opts)
commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
end
def build_user
@project.team.users.sample
end
......@@ -131,8 +146,8 @@ class Gitlab::Seeder::Builds
end
Gitlab::Seeder.quiet do
Project.all.sample(10).each do |project|
project_builds = Gitlab::Seeder::Builds.new(project)
Project.all.sample(5).each do |project|
project_builds = Gitlab::Seeder::Pipelines.new(project)
project_builds.seed!
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
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToNoteDiscussionId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :notes, :discussion_id
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'"
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: 20160819221833) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -90,6 +90,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
t.boolean "koding_enabled"
t.string "koding_url"
end
create_table "audit_events", force: :cascade do |t|
......@@ -569,6 +571,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
......@@ -701,6 +704,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
......@@ -785,6 +789,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|
......@@ -1151,4 +1156,4 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "u2f_registrations", "users"
end
end
\ No newline at end of file
......@@ -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
......
......@@ -15,6 +15,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].
......
doc/integration/img/koding_build-in-progress.png

69.3 KB

doc/integration/img/koding_build-logs.png

257 KB

doc/integration/img/koding_build-success.png

298 KB

doc/integration/img/koding_commit-koding.yml.png

296 KB

doc/integration/img/koding_different-stack-on-mr-try.png

326 KB

doc/integration/img/koding_edit-on-ide.png

323 KB

doc/integration/img/koding_enable-koding.png

71.8 KB

doc/integration/img/koding_landing.png

262 KB

doc/integration/img/koding_open-gitlab-from-koding.png

31.8 KB

doc/integration/img/koding_run-in-ide.png

63.9 KB

doc/integration/img/koding_run-mr-in-ide.png

332 KB

doc/integration/img/koding_set-up-ide.png

203 KB

doc/integration/img/koding_stack-import.png

489 KB

doc/integration/img/koding_start-build.png

103 KB

# 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.
......@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v3.3.3
sudo -u git -H git checkout v3.4.0
```
### 5. Update gitlab-workhorse
......
# 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
......
......@@ -97,6 +97,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
......
......@@ -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
......
......@@ -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,
......
......@@ -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)
......
......@@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler'
module Gitlab
module Email
module Handler
HANDLERS = [CreateNoteHandler, CreateIssueHandler]
# The `CreateIssueHandler` feature is disabled for the time being.
HANDLERS = [CreateNoteHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
......
......@@ -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)
......
......@@ -237,6 +237,56 @@ describe AutocompleteController do
end
end
context 'authorized projects apply limit' do
before do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
authorized_project.team << [user, :master]
authorized_project2.team << [user, :master]
authorized_project3.team << [user, :master]
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
end
describe 'GET #projects with project ID' do
before do
get(:projects, project_id: project.id)
end
let(:body) { JSON.parse(response.body) }
it do
expect(body).to be_kind_of(Array)
expect(body.size).to eq 3 # Of a total of 4
end
end
end
context 'authorized projects with offset' do
before do
authorized_project2 = create(:project)
authorized_project3 = create(:project)
authorized_project.team << [user, :master]
authorized_project2.team << [user, :master]
authorized_project3.team << [user, :master]
end
describe 'GET #projects with project ID and offset_id' do
before do
get(:projects, project_id: project.id, offset_id: authorized_project.id)
end
let(:body) { JSON.parse(response.body) }
it do
expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
end
end
end
context 'authorized projects without admin_issue ability' do
before(:each) do
authorized_project.team << [user, :guest]
......
......@@ -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
......
......@@ -525,7 +525,7 @@ describe 'Issues', feature: true do
end
end
describe 'new issue by email' do
xdescribe 'new issue by email' do
shared_examples 'show the email in the modal' do
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
......
......@@ -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 'Delete branch', feature: true, js: true do
include WaitForAjax
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
login_as user
visit namespace_project_branches_path(project.namespace, project)
end
it 'destroys tooltip' do
first('.remove-row').hover
expect(page).to have_selector('.tooltip')
first('.remove-row').click
wait_for_ajax
expect(page).not_to have_selector('.tooltip')
end
end
......@@ -20,7 +20,7 @@ describe 'Branches', feature: true do
describe 'Find branches' do
it 'shows filtered branches', js: true do
visit namespace_project_branches_path(project.namespace, project, project.id)
visit namespace_project_branches_path(project.namespace, project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
......
require 'spec_helper'
include WaitForAjax
describe 'Cherry-pick Commits' do
let(:project) { create(:project) }
......@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
before do
login_as :user
project.team << [@user, :master]
visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
end
context "I cherry-pick a commit" do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
......@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a merge commit" do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
......@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit that was previously cherry-picked" do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
......@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit in a new merge request" do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
......@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
end
end
context "I cherry-pick a commit from a different branch", js: true do
it do
find('.commit-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'master'
end
wait_for_ajax
page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
click_link 'feature'
end
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
end
expect(page).to have_content('The commit has been successfully cherry-picked.')
end
end
end
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
RSpec.shared_examples "protected branches > access control > CE" do
ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
allowed_to_push_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can push to them" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
find(".js-allowed-to-push").click
within('.js-allowed-to-push-container') { click_on access_type_name }
end
wait_for_ajax
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end
end
ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can merge to" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
allowed_to_merge_button = find(".js-allowed-to-merge")
unless allowed_to_merge_button.text == access_type_name
allowed_to_merge_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can merge to them" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
find(".js-allowed-to-merge").click
within('.js-allowed-to-merge-container') { click_on access_type_name }
end
wait_for_ajax
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
end
end
end
require 'spec_helper'
Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
include WaitForAjax
......@@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do
end
describe "access control" do
ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
allowed_to_push_button = find(".js-allowed-to-push")
unless allowed_to_push_button.text == access_type_name
allowed_to_push_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can push to them" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
find(".js-allowed-to-push").click
within('.js-allowed-to-push-container') { click_on access_type_name }
end
wait_for_ajax
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end
end
ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can merge to" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
allowed_to_merge_button = find(".js-allowed-to-merge")
unless allowed_to_merge_button.text == access_type_name
allowed_to_merge_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can merge to them" do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
find(".js-allowed-to-merge").click
within('.js-allowed-to-merge-container') { click_on access_type_name }
end
wait_for_ajax
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
end
end
include_examples "protected branches > access control > CE"
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 }
......
require 'spec_helper'
describe "Dashboard > User sorts todos", feature: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
before do
create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
merge_request_1.labels << label_1
issue_3.labels << label_1
issue_2.labels << label_3
issue_1.labels << label_2
project.team << [user, :developer]
login_as(user)
visit dashboard_todos_path
end
it "sorts with oldest created todos first" do
click_link "Last created"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("merge_request_1")
expect(results_list.all('p')[1]).to have_content("issue_1")
expect(results_list.all('p')[2]).to have_content("issue_3")
expect(results_list.all('p')[3]).to have_content("issue_2")
expect(results_list.all('p')[4]).to have_content("issue_4")
end
it "sorts with newest created todos first" do
click_link "Oldest created"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_4")
expect(results_list.all('p')[1]).to have_content("issue_2")
expect(results_list.all('p')[2]).to have_content("issue_3")
expect(results_list.all('p')[3]).to have_content("issue_1")
expect(results_list.all('p')[4]).to have_content("merge_request_1")
end
it "sorts by priority" do
click_link "Priority"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_3")
expect(results_list.all('p')[1]).to have_content("merge_request_1")
expect(results_list.all('p')[2]).to have_content("issue_1")
expect(results_list.all('p')[3]).to have_content("issue_2")
expect(results_list.all('p')[4]).to have_content("issue_4")
end
end
......@@ -51,6 +51,28 @@ describe MoveToProjectFinder do
expect(subject.execute(project).to_a).to eq([other_reporter_project])
end
it 'returns a page of projects ordered by id in descending order' do
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
reporter_project.team << [user, :reporter]
developer_project.team << [user, :developer]
master_project.team << [user, :master]
expect(subject.execute(project).to_a).to eq([master_project, developer_project])
end
it 'returns projects after the given offset id' do
stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
reporter_project.team << [user, :reporter]
developer_project.team << [user, :developer]
master_project.team << [user, :master]
expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
end
end
context 'search' do
......
require 'spec_helper'
describe TodosFinder do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:finder) { described_class }
before { project.team << [user, :developer] }
describe '#sort' do
context 'by date' do
let!(:todo1) { create(:todo, user: user, project: project) }
let!(:todo2) { create(:todo, user: user, project: project) }
let!(:todo3) { create(:todo, user: user, project: project) }
it 'sorts with oldest created first' do
todos = finder.new(user, { sort: 'id_asc' }).execute
expect(todos.first).to eq(todo1)
expect(todos.second).to eq(todo2)
expect(todos.third).to eq(todo3)
end
it 'sorts with newest created first' do
todos = finder.new(user, { sort: 'id_desc' }).execute
expect(todos.first).to eq(todo3)
expect(todos.second).to eq(todo2)
expect(todos.third).to eq(todo1)
end
end
it "sorts by priority" do
label_1 = create(:label, title: 'label_1', project: project, priority: 1)
label_2 = create(:label, title: 'label_2', project: project, priority: 2)
label_3 = create(:label, title: 'label_3', project: project, priority: 3)
issue_1 = create(:issue, title: 'issue_1', project: project)
issue_2 = create(:issue, title: 'issue_2', project: project)
issue_3 = create(:issue, title: 'issue_3', project: project)
issue_4 = create(:issue, title: 'issue_4', project: project)
merge_request_1 = create(:merge_request, source_project: project)
merge_request_1.labels << label_1
# Covers the case where Todo has more than one label
issue_3.labels << label_1
issue_3.labels << label_3
issue_2.labels << label_3
issue_1.labels << label_2
todo_1 = create(:todo, user: user, project: project, target: issue_4)
todo_2 = create(:todo, user: user, project: project, target: issue_2)
todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
todo_4 = create(:todo, user: user, project: project, target: issue_1)
todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
todos = finder.new(user, { sort: 'priority' }).execute
expect(todos.first).to eq(todo_3)
expect(todos.second).to eq(todo_5)
expect(todos.third).to eq(todo_4)
expect(todos.fourth).to eq(todo_2)
expect(todos.fifth).to eq(todo_1)
end
end
end
end
......@@ -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
......@@ -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
......
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
Projects
%i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Go to project
%button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
%i.fa.fa-times.dropdown-menu-close-icon
.dropdown-input
%input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
%i.fa.fa-search.dropdown-input-search
.dropdown-content
.dropdown-loading
%i.fa.fa-spinner.fa-spin
.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 jquery */
/*= require gl_dropdown */
/*= require turbolinks */
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
const ARROW_KEYS = {
DOWN: 40,
UP: 38,
ENTER: 13,
ESC: 27
};
let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
$('body').trigger({
type: 'keydown',
which: ARROW_KEYS[direction],
keyCode: ARROW_KEYS[direction]
});
i++;
if (i <= steps) {
navigateWithKeys(direction, steps, cb, i);
} else {
cb();
}
};
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
beforeEach(() => {
fixture.load('gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
this.projectsData = fixture.load('projects.json')[0];
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
data: this.projectsData,
text: (project) => {
(project.name_with_namespace || project.name);
},
id: (project) => {
project.id;
}
});
});
afterEach(() => {
$('body').unbind('keydown');
this.dropdownContainerElement.unbind('keyup');
});
it('should open on click', () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
this.dropdownButtonElement.click();
expect(this.dropdownContainerElement).toHaveClass('open');
});
describe('that is open', () => {
beforeEach(() => {
this.dropdownButtonElement.click();
});
it('should select a following item on DOWN keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
it('should select a previous item on UP keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
});
it('should click the selected item on ENTER keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open')
let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
expect(link).toHaveClass('is-active');
let linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
});
});
});
it('should close on ESC keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open');
this.dropdownContainerElement.trigger({
type: 'keyup',
which: ARROW_KEYS.ESC,
keyCode: ARROW_KEYS.ESC
});
expect(this.dropdownContainerElement).not.toHaveClass('open');
});
});
});
})();
//= 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);
});
});
})();
......@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
expect(list.find(a1).text()).toBe(' Issues assigned to me ');
expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
expect(list.find(a2).text()).toBe(" Issues I've created ");
expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
......
......@@ -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
......@@ -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
{
......
require 'spec_helper'
require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
......
......@@ -493,7 +493,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
......@@ -740,7 +745,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
......
......@@ -171,6 +171,70 @@ describe Ability, lib: true do
end
end
shared_examples_for ".project_abilities" do |enable_request_store|
before do
RequestStore.begin! if enable_request_store
end
after do
if enable_request_store
RequestStore.end!
RequestStore.clear!
end
end
describe '.project_abilities' do
let!(:project) { create(:empty_project, :public) }
let!(:user) { create(:user) }
it 'returns permissions for admin user' do
admin = create(:admin)
results = described_class.project_abilities(admin, project)
expect(results.count).to eq(68)
end
it 'returns permissions for an owner' do
results = described_class.project_abilities(project.owner, project)
expect(results.count).to eq(68)
end
it 'returns permissions for a master' do
project.team << [user, :master]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(60)
end
it 'returns permissions for a developer' do
project.team << [user, :developer]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(44)
end
it 'returns permissions for a guest' do
project.team << [user, :guest]
results = described_class.project_abilities(user, project)
expect(results.count).to eq(21)
end
end
end
describe '.project_abilities with RequestStore' do
it_behaves_like ".project_abilities", true
end
describe '.project_abilities without RequestStore' do
it_behaves_like ".project_abilities", false
end
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
......
require 'spec_helper'
describe BroadcastMessage, models: true do
include ActiveSupport::Testing::TimeHelpers
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
......
......@@ -124,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
......
......@@ -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) }
......
require 'spec_helper'
describe Network::Graph, models: true do
let(:project) { create(:project) }
let!(:note_on_commit) { create(:note_on_commit, project: project) }
it '#initialize' do
graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
end
......@@ -247,7 +247,7 @@ describe Project, models: true do
end
end
describe "#new_issue_address" do
xdescribe "#new_issue_address" do
let(:project) { create(:empty_project, path: "somewhere") }
let(:user) { create(:user) }
......
......@@ -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 '#revert' 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
......
......@@ -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({
......
......@@ -1113,6 +1113,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)
......
......@@ -33,6 +33,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
......
......@@ -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::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
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
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