Commit 8b05e8c2 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into diff-line-comment-vuejs

# Conflicts:
#	db/schema.rb
parents b5d0346a d97c8309
No related merge requests found
...@@ -11,6 +11,7 @@ v 8.11.0 (unreleased) ...@@ -11,6 +11,7 @@ v 8.11.0 (unreleased)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz) - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948 - Update to Ruby 2.3.1. !4948
- Add Issues Board !5548 - Add Issues Board !5548
- Allow resolving merge conflicts in the UI !5479
- Improve diff performance by eliminating redundant checks for text blobs - Improve diff performance by eliminating redundant checks for text blobs
- Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi) - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
- Convert switch icon into icon font (ClemMakesApps) - Convert switch icon into icon font (ClemMakesApps)
...@@ -20,6 +21,7 @@ v 8.11.0 (unreleased) ...@@ -20,6 +21,7 @@ v 8.11.0 (unreleased)
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell) - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository - 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) - 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) - Ignore URLs starting with // in Markdown links !5677 (winniehell)
- Fix CI status icon link underline (ClemMakesApps) - Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented - The Repository class is now instrumented
...@@ -29,6 +31,8 @@ v 8.11.0 (unreleased) ...@@ -29,6 +31,8 @@ v 8.11.0 (unreleased)
- Cache highlighted diff lines for merge requests - Cache highlighted diff lines for merge requests
- Pre-create all builds for a Pipeline when the new Pipeline is created !5295 - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
- Allow merge request diff notes and discussions to be explicitly marked as resolved - Allow merge request diff notes and discussions to be explicitly marked as resolved
- 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' - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Show member roles to all users on members page - Show member roles to all users on members page
- Project.visible_to_user is instrumented again - Project.visible_to_user is instrumented again
...@@ -52,8 +56,10 @@ v 8.11.0 (unreleased) ...@@ -52,8 +56,10 @@ v 8.11.0 (unreleased)
- Show deployment status on merge requests with external URLs - Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny) - Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission - Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
- Add green outline to New Branch button. !5447 (winniehell) - Add green outline to New Branch button. !5447 (winniehell)
- Optimize generating of cache keys for issues and notes - Optimize generating of cache keys for issues and notes
- Fix repository push email formatting in Outlook
- Improve performance of syntax highlighting Markdown code blocks - Improve performance of syntax highlighting Markdown code blocks
- Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
- Remove delay when hitting "Reply..." button on page with a lot of discussions - Remove delay when hitting "Reply..." button on page with a lot of discussions
...@@ -65,6 +71,7 @@ v 8.11.0 (unreleased) ...@@ -65,6 +71,7 @@ v 8.11.0 (unreleased)
- Update version_sorter and use new interface for faster tag sorting - Update version_sorter and use new interface for faster tag sorting
- Optimize checking if a user has read access to a list of issues !5370 - 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 - Store all DB secrets in secrets.yml, under descriptive names !5274
- Fix syntax highlighting in file editor
- Nokogiri's various parsing methods are now instrumented - Nokogiri's various parsing methods are now instrumented
- Add archived badge to project list !5798 - Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor) - Add simple identifier to public SSH keys (muteor)
...@@ -82,6 +89,8 @@ v 8.11.0 (unreleased) ...@@ -82,6 +89,8 @@ v 8.11.0 (unreleased)
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le) - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members` - Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook - Add pipeline events hook
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Bump gitlab_git to speedup DiffCollection iterations - Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich) - Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell) - Make branches sortable without push permission !5462 (winniehell)
...@@ -104,6 +113,7 @@ v 8.11.0 (unreleased) ...@@ -104,6 +113,7 @@ v 8.11.0 (unreleased)
- Add commit stats in commit api. !5517 (dixpac) - Add commit stats in commit api. !5517 (dixpac)
- Add CI configuration button on project page - Add CI configuration button on project page
- Fix merge request new view not changing code view rendering style - Fix merge request new view not changing code view rendering style
- edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi) - Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved - The performance of the project dropdown used for moving issues has been improved
- Fix skip_repo parameter being ignored when destroying a namespace - Fix skip_repo parameter being ignored when destroying a namespace
...@@ -127,10 +137,13 @@ v 8.11.0 (unreleased) ...@@ -127,10 +137,13 @@ v 8.11.0 (unreleased)
- Sort folders with submodules in Files view !5521 - Sort folders with submodules in Files view !5521
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 - 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 auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Add pipelines tab to merge requests
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with - 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 - Ensure file editing in UI does not overwrite commited changes without warning user
- Eliminate unneeded calls to Repository#blob_at when listing commits with no path - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
v 8.10.6 v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781 - Upgrade Rails to 4.2.7.1 for security fixes. !5781
......
...@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2' ...@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository # Extracting information from a git repository
# Provide access to Gitlab::Git library # Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.4.5' gem 'gitlab_git', '~> 10.4.7'
# LDAP Auth # LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes # GitLab fork with several improvements to original library. For full list of changes
...@@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0' ...@@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
gem 'rack-attack', '~> 4.3.1' gem 'rack-attack', '~> 4.3.1'
# Ace editor # Ace editor
gem 'ace-rails-ap', '~> 4.0.2' gem 'ace-rails-ap', '~> 4.1.0'
# Keyboard shortcuts # Keyboard shortcuts
gem 'mousetrap-rails', '~> 1.4.6' gem 'mousetrap-rails', '~> 1.4.6'
......
...@@ -2,7 +2,7 @@ GEM ...@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
ace-rails-ap (4.0.2) ace-rails-ap (4.1.0)
actionmailer (4.2.7.1) actionmailer (4.2.7.1)
actionpack (= 4.2.7.1) actionpack (= 4.2.7.1)
actionview (= 4.2.7.1) actionview (= 4.2.7.1)
...@@ -278,7 +278,7 @@ GEM ...@@ -278,7 +278,7 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab_git (10.4.5) gitlab_git (10.4.7)
activesupport (~> 4.0) activesupport (~> 4.0)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
...@@ -798,7 +798,7 @@ PLATFORMS ...@@ -798,7 +798,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
RedCloth (~> 4.3.2) RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.0.2) ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0) activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4) acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8) addressable (~> 2.3.8)
...@@ -857,7 +857,7 @@ DEPENDENCIES ...@@ -857,7 +857,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
github-markup (~> 1.4) github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.4.5) gitlab_git (~> 10.4.7)
gitlab_meta (= 7.0) gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
......
window.gl = window.gl || {};
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
class AbuseReports {
constructor() {
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
$(document)
.off('click', MESSAGE_CELL_SELECTOR)
.on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
}
truncateLongMessage() {
const $messageCellElement = $(this);
const reportMessage = $messageCellElement.text();
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
toggleMessageTruncation() {
const $messageCellElement = $(this);
const originalMessage = $messageCellElement.data('original-message');
if (!originalMessage) return;
if ($messageCellElement.data('message-truncated') === 'true') {
$messageCellElement.data('message-truncated', 'false');
$messageCellElement.text(originalMessage);
} else {
$messageCellElement.data('message-truncated', 'true');
$messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
}
}
}
global.AbuseReports = AbuseReports;
})(window.gl);
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
/*= require bootstrap/tooltip */ /*= require bootstrap/tooltip */
/*= require bootstrap/popover */ /*= require bootstrap/popover */
/*= require select2 */ /*= require select2 */
/*= require ace/ace */ /*= require ace-rails-ap */
/*= require ace/ext-searchbox */ /*= require ace/ext-searchbox */
/*= require underscore */ /*= require underscore */
/*= require dropzone */ /*= require dropzone */
......
(function() { (function() {
this.AwardsHandler = (function() { this.AwardsHandler = (function() {
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
function AwardsHandler() { function AwardsHandler() {
this.aliases = gl.emojiAliases(); this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
...@@ -130,7 +131,7 @@ ...@@ -130,7 +131,7 @@
counter = $emojiButton.find('.js-counter'); counter = $emojiButton.find('.js-counter');
counter.text(parseInt(counter.text()) + 1); counter.text(parseInt(counter.text()) + 1);
$emojiButton.addClass('active'); $emojiButton.addClass('active');
this.addMeToUserList(votesBlock, emoji); this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton); return this.animateEmoji($emojiButton);
} }
} else { } else {
...@@ -176,11 +177,11 @@ ...@@ -176,11 +177,11 @@
counterNumber = parseInt(counter.text(), 10); counterNumber = parseInt(counter.text(), 10);
if (counterNumber > 1) { if (counterNumber > 1) {
counter.text(counterNumber - 1); counter.text(counterNumber - 1);
this.removeMeFromUserList($emojiButton, emoji); this.removeYouFromUserList($emojiButton, emoji);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
$emojiButton.tooltip('destroy'); $emojiButton.tooltip('destroy');
counter.text('0'); counter.text('0');
this.removeMeFromUserList($emojiButton, emoji); this.removeYouFromUserList($emojiButton, emoji);
if ($emojiButton.parents('.note').length) { if ($emojiButton.parents('.note').length) {
this.removeEmoji($emojiButton); this.removeEmoji($emojiButton);
} }
...@@ -204,43 +205,48 @@ ...@@ -204,43 +205,48 @@
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
}; };
AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) { AwardsHandler.prototype.toSentence = function(list) {
if(list.length <= 2){
return list.join(' and ');
}
else{
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
}
};
AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
var authors, awardBlock, newAuthors, originalTitle; var authors, awardBlock, newAuthors, originalTitle;
awardBlock = $emojiButton; awardBlock = $emojiButton;
originalTitle = this.getAwardTooltip(awardBlock); originalTitle = this.getAwardTooltip(awardBlock);
authors = originalTitle.split(', '); authors = originalTitle.split(FROM_SENTENCE_REGEX);
authors.splice(authors.indexOf('me'), 1); authors.splice(authors.indexOf('You'), 1);
newAuthors = authors.join(', '); return awardBlock
awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors); .closest('.js-emoji-btn')
return this.resetTooltip(awardBlock); .removeData('title')
}; .removeAttr('data-title')
.removeAttr('data-original-title')
AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) { .attr('title', this.toSentence(authors))
.tooltip('fixTitle');
};
AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
var awardBlock, origTitle, users; var awardBlock, origTitle, users;
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
origTitle = this.getAwardTooltip(awardBlock); origTitle = this.getAwardTooltip(awardBlock);
users = []; users = [];
if (origTitle) { if (origTitle) {
users = origTitle.trim().split(', '); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.push('me'); users.unshift('You');
awardBlock.attr('title', users.join(', ')); return awardBlock
return this.resetTooltip(awardBlock); .attr('title', this.toSentence(users))
}; .tooltip('fixTitle');
AwardsHandler.prototype.resetTooltip = function(award) {
var cb;
award.tooltip('destroy');
cb = function() {
return award.tooltip();
};
return setTimeout(cb, 200);
}; };
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
var $emojiButton, buttonHtml, emojiCssClass; var $emojiButton, buttonHtml, emojiCssClass;
emojiCssClass = this.resolveNameToCssClass(emoji); emojiCssClass = this.resolveNameToCssClass(emoji);
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
$emojiButton = $(buttonHtml); $emojiButton = $(buttonHtml);
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
this.animateEmoji($emojiButton); this.animateEmoji($emojiButton);
......
Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
Vue.nextTick(() => {
setTimeout(() => { setTimeout(() => {
Vue.activeResources--; Vue.activeResources--;
}, 500); }, 500);
});
next(); next();
}); });
...@@ -196,6 +196,9 @@ ...@@ -196,6 +196,9 @@
case 'edit': case 'edit':
new Labels(); new Labels();
} }
case 'abuse_reports':
new gl.AbuseReports();
break;
} }
break; break;
case 'dashboard': case 'dashboard':
......
...@@ -104,9 +104,12 @@ ...@@ -104,9 +104,12 @@
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend')); return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
}); });
}; };
return gl.text.removeListeners = function(form) { gl.text.removeListeners = function(form) {
return $('.js-md', form).off(); return $('.js-md', form).off();
}; };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
})(window); })(window);
}).call(this); }).call(this);
...@@ -45,7 +45,6 @@ ...@@ -45,7 +45,6 @@
.line_content { .line_content {
padding-left: 0.5em; padding-left: 0.5em;
padding-right: 0.5em; padding-right: 0.5em;
white-space: pre;
&.old { &.old {
background-color: $line-removed; background-color: $line-removed;
...@@ -71,6 +70,10 @@ ...@@ -71,6 +70,10 @@
} }
} }
pre {
margin: 0;
}
span.highlight_word { span.highlight_word {
background-color: #fafe3d !important; background-color: #fafe3d !important;
} }
......
...@@ -72,7 +72,6 @@ ...@@ -72,7 +72,6 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
// Users List // Users List
.users-list { .users-list {
...@@ -98,3 +97,44 @@ ...@@ -98,3 +97,44 @@
} }
} }
} }
.abuse-reports {
.table {
table-layout: fixed;
}
.subheading {
padding-bottom: $gl-padding;
}
.message {
word-wrap: break-word;
}
.btn {
white-space: normal;
padding: $gl-btn-padding;
}
th {
width: 15%;
&.wide {
width: 55%;
}
}
@media (max-width: $screen-sm-max) {
th {
width: 100%;
}
td {
width: 100%;
float: left;
}
}
.no-reports {
.emoji-icon {
margin-left: $btn-side-margin;
margin-top: 3px;
}
span {
font-size: 19px;
}
}
}
...@@ -228,3 +228,9 @@ ...@@ -228,3 +228,9 @@
} }
} }
} }
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
}
}
\ No newline at end of file
...@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def destroy def destroy
TodoService.new.mark_todos_as_done([todo], current_user) TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
...@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private private
def todo
@todo ||= find_todos.find(params[:id])
end
def find_todos def find_todos
@todos ||= TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params).execute
end end
......
...@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful # A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place. # registration, which is then used while 2FA authentication is taking place.
def create_u2f def create_u2f
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges]) @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
if @u2f_registration.persisted? if @u2f_registration.persisted?
session.delete(:challenges) session.delete(:challenges)
redirect_to profile_account_path, notice: "Your U2F device was registered!" redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
else else
@qr_code = build_qr_code @qr_code = build_qr_code
setup_u2f_registration setup_u2f_registration
...@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API # Actual communication is performed using a Javascript API
def setup_u2f_registration def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new @u2f_registration ||= U2fRegistration.new
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle) @u2f_registrations = current_user.u2f_registrations
u2f = U2F::U2F.new(u2f_app_id) u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests registration_requests = u2f.registration_requests
sign_requests = u2f.authentication_requests(@registration_key_handles) sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
session[:challenges] = registration_requests.map(&:challenge) session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests, register_requests: registration_requests,
sign_requests: sign_requests }) sign_requests: sign_requests })
end end
def u2f_registration_params
params.require(:u2f_registration).permit(:device_response, :name)
end
end end
class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
u2f_registration.destroy
redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
end
end
...@@ -11,17 +11,14 @@ module BlobHelper ...@@ -11,17 +11,14 @@ module BlobHelper
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob return unless blob
from_mr = options[:from_merge_request_id]
link_opts = {}
link_opts[:from_merge_request_id] = from_mr if from_mr
edit_path = namespace_project_edit_blob_path(project.namespace, project, edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path), tree_join(ref, path),
link_opts) options[:link_opts])
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
......
...@@ -114,9 +114,17 @@ module IssuesHelper ...@@ -114,9 +114,17 @@ module IssuesHelper
end end
def award_user_list(awards, current_user) def award_user_list(awards, current_user)
awards.map do |award| names = awards.map do |award|
award.user == current_user ? 'me' : award.user.name award.user == current_user ? 'You' : award.user.name
end.join(', ') end
# Take first 9 OR current user + first 9
current_user_name = names.delete('You')
names = names.first(9).insert(0, current_user_name).compact
names << "#{awards.size - names.size} more." if awards.size > names.size
names.to_sentence
end end
def award_active_class(awards, current_user) def award_active_class(awards, current_user)
......
...@@ -3,18 +3,19 @@ ...@@ -3,18 +3,19 @@
class U2fRegistration < ActiveRecord::Base class U2fRegistration < ActiveRecord::Base
belongs_to :user belongs_to :user
def self.register(user, app_id, json_response, challenges) def self.register(user, app_id, params, challenges)
u2f = U2F::U2F.new(app_id) u2f = U2F::U2F.new(app_id)
registration = self.new registration = self.new
begin begin
response = U2F::RegisterResponse.load_from_json(json_response) response = U2F::RegisterResponse.load_from_json(params[:device_response])
registration_data = u2f.register!(challenges, response) registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate, registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle, key_handle: registration_data.key_handle,
public_key: registration_data.public_key, public_key: registration_data.public_key,
counter: registration_data.counter, counter: registration_data.counter,
user: user) user: user,
name: params[:name])
rescue JSON::ParserError, NoMethodError, ArgumentError rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e rescue U2F::Error => e
......
...@@ -142,7 +142,11 @@ class TodoService ...@@ -142,7 +142,11 @@ class TodoService
# When user marks some todos as done # When user marks some todos as done
def mark_todos_as_done(todos, current_user) def mark_todos_as_done(todos, current_user)
todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) mark_todos_as_done_by_ids(todos.select(&:id), current_user)
end
def mark_todos_as_done_by_ids(ids, current_user)
todos = current_user.todos.where(id: ids)
marked_todos = todos.update_all(state: :done) marked_todos = todos.update_all(state: :done)
current_user.update_todos_count_cache current_user.update_todos_count_cache
......
- reporter = abuse_report.reporter - reporter = abuse_report.reporter
- user = abuse_report.user - user = abuse_report.user
%tr %tr
%th.visible-xs-block.visible-sm-block
%strong User
%td %td
- if user - if user
= link_to user.name, user = link_to user.name, user
...@@ -9,6 +11,7 @@ ...@@ -9,6 +11,7 @@
- else - else
(removed) (removed)
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Reported by
- if reporter - if reporter
= link_to reporter.name, reporter = link_to reporter.name, reporter
- else - else
...@@ -16,16 +19,16 @@ ...@@ -16,16 +19,16 @@
.light.small .light.small
= time_ago_with_tooltip(abuse_report.created_at) = time_ago_with_tooltip(abuse_report.created_at)
%td %td
%strong.subheading.visible-xs-block.visible-sm-block Message
.message
= markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td %td
- if user - if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
%td
- if user && !user.blocked? - if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
- else - else
.btn.btn-xs.disabled .btn.btn-sm.disabled.btn-block
Already Blocked Already Blocked
= link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
- page_title "Abuse Reports" - page_title 'Abuse Reports'
%h3.page-title Abuse Reports %h3.page-title Abuse Reports
%hr %hr
- if @abuse_reports.present? .abuse-reports
- if @abuse_reports.present?
.table-holder .table-holder
%table.table %table.table
%thead %thead.hidden-sm.hidden-xs
%tr %tr
%th User %th User
%th Reported by %th Reported by
%th Message %th.wide Message
%th Primary action %th Action
%th
= render @abuse_reports = render @abuse_reports
= paginate @abuse_reports - else
- else .no-reports
%h4 There are no abuse reports %span.pull-left
There are no abuse reports!
.pull-left
= emoji_icon 'tada'
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white %table.code.white
- diff_file.highlighted_diff_lines.each do |line| - diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true, email: true
- else - else
No preview for this file type No preview for this file type
%br %br
...@@ -60,13 +60,38 @@ ...@@ -60,13 +60,38 @@
two-factor authentication app before a U2F device. That way you'll always be able to two-factor authentication app before a U2F device. That way you'll always be able to
log in - even when you're using an unsupported browser. log in - even when you're using an unsupported browser.
.col-lg-9 .col-lg-9
%p
- if @registration_key_handles.present?
= icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present? - if @u2f_registration.errors.present?
= form_errors(@u2f_registration) = form_errors(@u2f_registration)
= render "u2f/register" = render "u2f/register"
%hr
%h5 U2F Devices (#{@u2f_registrations.length})
- if @u2f_registrations.present?
.table-responsive
%table.table.table-bordered.u2f-registrations
%colgroup
%col{ width: "50%" }
%col{ width: "30%" }
%col{ width: "20%" }
%thead
%tr
%th Name
%th Registered On
%th
%tbody
- @u2f_registrations.each do |registration|
%tr
%td= registration.name.presence || "<no name set>"
%td= registration.created_at.to_date.to_s(:medium)
%td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
- else
.settings-message.text-center
You don't have any U2F devices registered yet.
- if two_factor_skippable? - if two_factor_skippable?
:javascript :javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
......
...@@ -10,10 +10,9 @@ ...@@ -10,10 +10,9 @@
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project, - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
@merge_request.source_branch, diff_file.new_path, = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
from_merge_request_id: @merge_request.id, blob: blob, link_opts: link_opts)
skip_visible_check: true)
= view_file_btn(diff_commit.id, diff_file.new_path, project) = view_file_btn(diff_commit.id, diff_file.new_path, project)
......
- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false) - plain = local_assigns.fetch(:plain, false)
- type = line.type - type = line.type
- line_code = diff_file.line_code(line) unless plain - line_code = diff_file.line_code(line) unless plain
...@@ -22,4 +23,8 @@ ...@@ -22,4 +23,8 @@
= link_text = link_text
- else - else
%a{href: "##{line_code}", data: { linenumber: link_text }} %a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type) %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
%pre= diff_line_content(line.text, type)
- else
= diff_line_content(line.text, type)
...@@ -28,9 +28,14 @@ ...@@ -28,9 +28,14 @@
%script#js-register-u2f-registered{ type: "text/template" } %script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10 %div.row.append-bottom-10
%p Your device was successfully set up! Click this button to register with the GitLab server. .col-md-12
%p Your device was successfully set up! Give it a name and register it with the GitLab server.
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
= hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response" .row.append-bottom-10
.col-md-3
= text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
.col-md-3
= hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
= submit_tag "Register U2F Device", class: "btn btn-success" = submit_tag "Register U2F Device", class: "btn btn-success"
:javascript :javascript
......
...@@ -375,6 +375,8 @@ Rails.application.routes.draw do ...@@ -375,6 +375,8 @@ Rails.application.routes.draw do
patch :skip patch :skip
end end
end end
resources :u2f_registrations, only: [:destroy]
end end
end end
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddColumnNameToU2fRegistrations < 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 :u2f_registrations, :name, :string
end
end
...@@ -1020,6 +1020,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do ...@@ -1020,6 +1020,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.integer "user_id" t.integer "user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "name"
end end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
......
...@@ -26,6 +26,7 @@ following locations: ...@@ -26,6 +26,7 @@ following locations:
- [Open source license templates](licenses.md) - [Open source license templates](licenses.md)
- [Namespaces](namespaces.md) - [Namespaces](namespaces.md)
- [Notes](notes.md) (comments) - [Notes](notes.md) (comments)
- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks - [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md) - [Project Access Requests](access_requests.md)
- [Project Members](members.md) - [Project Members](members.md)
......
...@@ -532,3 +532,49 @@ Example response: ...@@ -532,3 +532,49 @@ Example response:
"user": null "user": null
} }
``` ```
## Play a build
Triggers a manual action to start a build.
```
POST /projects/:id/builds/:build_id/play
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `build_id` | integer | yes | The ID of a build |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
```
Example of response
```json
{
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2015-12-24T16:51:14.000+01:00",
"id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"message": "Test the CI integration.",
"short_id": "0ff3ae19",
"title": "Test the CI integration."
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
"artifacts_file": null,
"finished_at": null,
"id": 69,
"name": "rubocop",
"ref": "master",
"runner": null,
"stage": "test",
"started_at": null,
"status": "started",
"tag": false,
"user": null
}
```
# Deployments API
## List project deployments
Get a list of deployments in a project.
```
GET /projects/:id/deployments
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
```
Example of response
```json
[
{
"created_at": "2016-08-11T07:36:40.222Z",
"deployable": {
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2016-08-11T09:36:01.000+02:00",
"id": "99d03678b90d914dbb1b109132516d71a4a03ea8",
"message": "Merge branch 'new-title' into 'master'\r\n\r\nUpdate README\r\n\r\n\r\n\r\nSee merge request !1",
"short_id": "99d03678",
"title": "Merge branch 'new-title' into 'master'\r"
},
"coverage": null,
"created_at": "2016-08-11T07:36:27.357Z",
"finished_at": "2016-08-11T07:36:39.851Z",
"id": 657,
"name": "deploy",
"ref": "master",
"runner": null,
"stage": "deploy",
"started_at": null,
"status": "success",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"website_url": ""
}
},
"environment": {
"external_url": "https://about.gitlab.com",
"id": 9,
"name": "production"
},
"id": 41,
"iid": 1,
"ref": "master",
"sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"id": 1,
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
}
},
{
"created_at": "2016-08-11T11:32:35.444Z",
"deployable": {
"commit": {
"author_email": "admin@example.com",
"author_name": "Administrator",
"created_at": "2016-08-11T13:28:26.000+02:00",
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
"short_id": "a91957a8",
"title": "Merge branch 'rename-readme' into 'master'\r"
},
"coverage": null,
"created_at": "2016-08-11T11:32:24.456Z",
"finished_at": "2016-08-11T11:32:35.145Z",
"id": 664,
"name": "deploy",
"ref": "master",
"runner": null,
"stage": "deploy",
"started_at": null,
"status": "success",
"tag": false,
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"bio": null,
"created_at": "2016-08-11T07:09:20.351Z",
"id": 1,
"is_admin": true,
"linkedin": "",
"location": null,
"name": "Administrator",
"skype": "",
"state": "active",
"twitter": "",
"username": "root",
"web_url": "http://localhost:3000/u/root",
"website_url": ""
}
},
"environment": {
"external_url": "https://about.gitlab.com",
"id": 9,
"name": "production"
},
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"user": {
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"id": 1,
"name": "Administrator",
"state": "active",
"username": "root",
"web_url": "http://localhost:3000/u/root"
}
}
]
```
## Get a specific deployment
```
GET /projects/:id/deployments/:deployment_id
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `deployment_id` | integer | yes | The ID of the deployment |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
```
Example of response
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": {
"id": 664,
"status": "success",
"stage": "deploy",
"name": "deploy",
"ref": "master",
"tag": false,
"coverage": null,
"created_at": "2016-08-11T11:32:24.456Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root",
"created_at": "2016-08-11T07:09:20.351Z",
"is_admin": true,
"bio": null,
"location": null,
"skype": "",
"linkedin": "",
"twitter": "",
"website_url": ""
},
"commit": {
"id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"short_id": "a91957a8",
"title": "Merge branch 'rename-readme' into 'master'\r",
"author_name": "Administrator",
"author_email": "admin@example.com",
"created_at": "2016-08-11T13:28:26.000+02:00",
"message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
},
"runner": null
}
}
```
# Pipelines API
## List project pipelines
> [Introduced][ce-5837] in GitLab 8.11
```
GET /projects/:id/pipelines
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
```
Example of response
```json
[
{
"id": 47,
"status": "pending",
"ref": "new-pipeline",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-16T10:23:19.007Z",
"updated_at": "2016-08-16T10:23:19.216Z",
"started_at": null,
"finished_at": null,
"committed_at": null,
"duration": null
},
{
"id": 48,
"status": "pending",
"ref": "new-pipeline",
"sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
"before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-16T10:23:21.184Z",
"updated_at": "2016-08-16T10:23:21.314Z",
"started_at": null,
"finished_at": null,
"committed_at": null,
"duration": null
}
]
```
## Get a single pipeline
> [Introduced][ce-5837] in GitLab 8.11
```
GET /projects/:id/pipelines/:pipeline_id
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
```
Example of response
```json
{
"id": 46,
"status": "success",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
## Retry failed builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11
```
POST /projects/:id/pipelines/:pipeline_id/retry
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
```
Response:
```json
{
"id": 46,
"status": "pending",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
## Cancel a pipelines builds
> [Introduced][ce-5837] in GitLab 8.11
```
POST /projects/:id/pipelines/:pipeline_id/cancel
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `pipeline_id` | integer | yes | The ID of a pipeline |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
```
Response:
```json
{
"id": 46,
"status": "canceled",
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"created_at": "2016-08-11T11:28:34.085Z",
"updated_at": "2016-08-11T11:32:35.169Z",
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null
}
```
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
...@@ -353,7 +353,7 @@ job_name: ...@@ -353,7 +353,7 @@ job_name:
| except | no | Defines a list of git refs for which build is not created | | except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select Runner | | tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | when | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list of build artifacts | | artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs | | cache | no | Define list of files that should be cached between subsequent runs |
......
...@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa ...@@ -16,7 +16,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation is authorized to submit Contributions on behalf of the Corporation, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of corporation here]." 4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). 5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
...@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa ...@@ -24,6 +24,6 @@ Subject to the terms and conditions of this Agreement, You hereby grant to GitLa
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". 7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8. It is your responsibility to notify GitLab B.V. when any change is required to the designation of employees not authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V.. 8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
...@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn' expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']") expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
end end
end end
......
...@@ -299,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps ...@@ -299,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end end
step 'I fill in issue search with \'Rock and roll\'' do step 'I fill in issue search with \'Rock and roll\'' do
filter_issue 'Description for issue' filter_issue 'Rock and roll'
end end
step 'I should see \'Bugfix1\' in issues' do step 'I should see \'Bugfix1\' in issues' do
......
...@@ -43,6 +43,7 @@ module API ...@@ -43,6 +43,7 @@ module API
mount ::API::CommitStatuses mount ::API::CommitStatuses
mount ::API::Commits mount ::API::Commits
mount ::API::DeployKeys mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments mount ::API::Environments
mount ::API::Files mount ::API::Files
mount ::API::Groups mount ::API::Groups
...@@ -56,6 +57,7 @@ module API ...@@ -56,6 +57,7 @@ module API
mount ::API::Milestones mount ::API::Milestones
mount ::API::Namespaces mount ::API::Namespaces
mount ::API::Notes mount ::API::Notes
mount ::API::Pipelines
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::Projects mount ::API::Projects
......
...@@ -189,6 +189,27 @@ module API ...@@ -189,6 +189,27 @@ module API
present build, with: Entities::Build, present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project) user_can_download_artifacts: can?(current_user, :read_build, user_project)
end end
desc 'Trigger a manual build' do
success Entities::Build
detail 'This feature was added in GitLab 8.11'
end
params do
requires :build_id, type: Integer, desc: 'The ID of a Build'
end
post ":id/builds/:build_id/play" do
authorize_read_builds!
build = get_build!(params[:build_id])
bad_request!("Unplayable Build") unless build.playable?
build.play(current_user)
status 200
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
end end
helpers do helpers do
......
module API
# Deployments RESTfull API endpoints
class Deployments < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
end
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
end
get ':id/deployments' do
authorize! :read_deployment, user_project
present paginate(user_project.deployments), with: Entities::Deployment
end
desc 'Gets a specific deployment' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
end
params do
requires :deployment_id, type: Integer, desc: 'The deployment ID'
end
get ':id/deployments/:deployment_id' do
authorize! :read_deployment, user_project
deployment = user_project.deployments.find(params[:deployment_id])
present deployment, with: Entities::Deployment
end
end
end
end
...@@ -502,8 +502,28 @@ module API ...@@ -502,8 +502,28 @@ module API
expose :key, :value expose :key, :value
end end
class Pipeline < Grape::Entity
expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration
end
class Environment < Grape::Entity class Environment < Grape::Entity
expose :id, :name, :external_url expose :id, :name, :external_url
expose :project, using: Entities::Project
end
class EnvironmentBasic < Grape::Entity
expose :id, :name, :external_url
end
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
expose :deployable, using: Entities::Build
end end
class RepoLicense < Grape::Entity class RepoLicense < Grape::Entity
......
module API
class Pipelines < Grape::API
before { authenticate! }
params do
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
present paginate(user_project.pipelines), with: Entities::Pipeline
end
desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
get ':id/pipelines/:pipeline_id' do
authorize! :read_pipeline, user_project
present pipeline, with: Entities::Pipeline
end
desc 'Retry failed builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/retry' do
authorize! :update_pipeline, user_project
pipeline.retry_failed(current_user)
present pipeline, with: Entities::Pipeline
end
desc 'Cancel all builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
post ':id/pipelines/:pipeline_id/cancel' do
authorize! :update_pipeline, user_project
pipeline.cancel_running
status 200
present pipeline.reload, with: Entities::Pipeline
end
end
helpers do
def pipeline
@pipeline ||= user_project.pipelines.find(params[:pipeline_id])
end
end
end
end
# == Schema Information
#
# Table name: commits
#
# id :integer not null, primary key
# project_id :integer
# ref :string(255)
# sha :string(255)
# before_sha :string(255)
# push_data :text
# created_at :datetime
# updated_at :datetime
# tag :boolean default(FALSE)
# yaml_errors :text
# committed_at :datetime
# gl_project_id :integer
#
FactoryGirl.define do FactoryGirl.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do factory :ci_empty_pipeline, class: Ci::Pipeline do
ref 'master' ref 'master'
......
...@@ -147,6 +147,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -147,6 +147,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 20) expect(page).to have_selector('.card', count: 20)
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource(spinner: false)
expect(page.find('.board-header')).to have_content('40') expect(page.find('.board-header')).to have_content('40')
expect(page).to have_selector('.card', count: 40) expect(page).to have_selector('.card', count: 40)
...@@ -165,6 +166,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -165,6 +166,8 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
find('.form-control').set issue1.title find('.form-control').set issue1.title
wait_for_vue_resource(spinner: false)
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
end end
end end
...@@ -176,7 +179,11 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -176,7 +179,11 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
find('.board-search-clear-btn').click find('.board-search-clear-btn').click
end
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6) expect(page).to have_selector('.card', count: 6)
end end
end end
...@@ -189,6 +196,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -189,6 +196,8 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.card', count: 5) expect(page).to have_selector('.card', count: 5)
end end
wait_for_vue_resource
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('3') expect(page.find('.board-header')).to have_content('3')
expect(page).to have_selector('.card', count: 3) expect(page).to have_selector('.card', count: 3)
...@@ -263,6 +272,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -263,6 +272,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'new list' do context 'new list' do
it 'shows all labels in new list dropdown' do it 'shows all labels in new list dropdown' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
expect(page).to have_content(planning.title) expect(page).to have_content(planning.title)
...@@ -273,6 +283,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -273,6 +283,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for label' do it 'creates new list for label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link testing.title click_link testing.title
...@@ -285,6 +296,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -285,6 +296,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Backlog label' do it 'creates new list for Backlog label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link backlog.title click_link backlog.title
...@@ -297,6 +309,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -297,6 +309,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'creates new list for Done label' do it 'creates new list for Done label' do
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link done.title click_link done.title
...@@ -314,6 +327,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -314,6 +327,7 @@ describe 'Issue Boards', feature: true, js: true do
end end
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link testing.title click_link testing.title
...@@ -333,6 +347,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -333,6 +347,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by author' do it 'filters by author' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Author') click_button('Author')
wait_for_ajax
page.within '.dropdown-menu-author' do page.within '.dropdown-menu-author' do
click_link(user2.name) click_link(user2.name)
...@@ -358,6 +373,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -358,6 +373,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by assignee' do it 'filters by assignee' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Assignee') click_button('Assignee')
wait_for_ajax
page.within '.dropdown-menu-assignee' do page.within '.dropdown-menu-assignee' do
click_link(user.name) click_link(user.name)
...@@ -383,6 +399,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -383,6 +399,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by milestone' do it 'filters by milestone' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Milestone') click_button('Milestone')
wait_for_ajax
page.within '.milestone-filter' do page.within '.milestone-filter' do
click_link(milestone.title) click_link(milestone.title)
...@@ -408,6 +425,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -408,6 +425,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by label' do it 'filters by label' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link(testing.title) click_link(testing.title)
...@@ -436,6 +454,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -436,6 +454,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link(testing.title) click_link(testing.title)
...@@ -460,8 +479,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -460,8 +479,9 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by multiple labels' do it 'filters by multiple labels' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within(find('.dropdown-menu-labels')) do
click_link(testing.title) click_link(testing.title)
wait_for_vue_resource(spinner: false) wait_for_vue_resource(spinner: false)
click_link(bug.title) click_link(bug.title)
...@@ -486,6 +506,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -486,6 +506,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by no label' do it 'filters by no label' do
page.within '.issues-filters' do page.within '.issues-filters' do
click_button('Label') click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do page.within '.dropdown-menu-labels' do
click_link("No Label") click_link("No Label")
...@@ -510,10 +531,13 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -510,10 +531,13 @@ describe 'Issue Boards', feature: true, js: true do
it 'filters by clicking label button on issue' do it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page).to have_selector('.card', count: 6) expect(page).to have_selector('.card', count: 6)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title) click_button(bug.title)
wait_for_vue_resource(spinner: false) wait_for_vue_resource(spinner: false)
end end
wait_for_vue_resource
page.within(find('.board', match: :first)) do page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('1') expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1) expect(page).to have_selector('.card', count: 1)
......
...@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -12,10 +12,12 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
def register_u2f_device(u2f_device = nil) def register_u2f_device(u2f_device = nil)
u2f_device ||= FakeU2fDevice.new(page) name = FFaker::Name.first_name
u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device' click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up') expect(page).to have_content('Your device was successfully set up')
fill_in "Pick a name", with: name
click_on 'Register U2F Device' click_on 'Register U2F Device'
u2f_device u2f_device
end end
...@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -40,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
describe 'when 2FA via OTP is enabled' do describe 'when 2FA via OTP is enabled' do
it 'allows registering a new device' do it 'allows registering a new device with a name' do
visit profile_account_path visit profile_account_path
manage_two_factor_authentication manage_two_factor_authentication
expect(page.body).to match("You've already enabled two-factor authentication using mobile") expect(page.body).to match("You've already enabled two-factor authentication using mobile")
register_u2f_device u2f_device = register_u2f_device
expect(page.body).to match(u2f_device.name)
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
end end
...@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -55,15 +58,31 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# First device # First device
manage_two_factor_authentication manage_two_factor_authentication
register_u2f_device first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
# Second device # Second device
manage_two_factor_authentication second_device = register_u2f_device
register_u2f_device
expect(page.body).to match('Your U2F device was registered') expect(page.body).to match('Your U2F device was registered')
expect(page.body).to match(first_device.name)
expect(page.body).to match(second_device.name)
expect(U2fRegistration.count).to eq(2)
end
it 'allows deleting a device' do
visit profile_account_path
manage_two_factor_authentication manage_two_factor_authentication
expect(page.body).to match('You have 2 U2F devices registered') expect(page.body).to match("You've already enabled two-factor authentication using mobile")
first_u2f_device = register_u2f_device
second_u2f_device = register_u2f_device
click_on "Delete", match: :first
expect(page.body).to match('Successfully deleted')
expect(page.body).not_to match(first_u2f_device.name)
expect(page.body).to match(second_u2f_device.name)
end end
end end
...@@ -208,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -208,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when a given U2F device has not been registered" do describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do it "does not allow logging in with that particular device" do
unregistered_device = FakeU2fDevice.new(page) unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user) login_as(user)
unregistered_device.respond_to_u2f_authentication unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Login Via U2F Device"
...@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -262,6 +281,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end end
it "deletes u2f registrations" do it "deletes u2f registrations" do
visit profile_account_path
expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1) expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
end end
end end
......
...@@ -69,18 +69,40 @@ describe BlobHelper do ...@@ -69,18 +69,40 @@ describe BlobHelper do
end end
describe "#edit_blob_link" do describe "#edit_blob_link" do
let(:project) { create(:project) } let(:namespace) { create(:namespace, name: 'gitlab' )}
let(:project) { create(:project, namespace: namespace) }
before do before do
allow(self).to receive(:current_user).and_return(double) allow(self).to receive(:current_user).and_return(double)
allow(self).to receive(:can_collaborate_with_project?).and_return(true)
end end
it 'verifies blob is text' do it 'verifies blob is text' do
expect(self).not_to receive(:blob_text_viewable?) expect(helper).not_to receive(:blob_text_viewable?)
button = edit_blob_link(project, 'refs/heads/master', 'README.md') button = edit_blob_link(project, 'refs/heads/master', 'README.md')
expect(button).to start_with('<button') expect(button).to start_with('<button')
end end
it 'uses the passed blob instead retrieve from repository' do
blob = project.repository.blob_at('refs/heads/master', 'README.md')
expect(project.repository).not_to receive(:blob_at)
edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
end
it 'returns a link with the proper route' do
link = edit_blob_link(project, 'master', 'README.md')
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
end
it 'returns a link with the passed link_opts on the expected route' do
link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
end
end end
end end
...@@ -62,6 +62,32 @@ describe IssuesHelper do ...@@ -62,6 +62,32 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") } it { is_expected.to eq("!1, !2, or !3") }
end end
describe '#award_user_list' do
let!(:awards) { build_list(:award_emoji, 15) }
it "returns a comma seperated list of 1-9 users" do
expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
end
it "displays the current user's name as 'You'" do
expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
end
it "truncates lists of larger than 9 users" do
expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
end
it "displays the current user in front of 0-9 other users" do
expect(award_user_list(awards, awards[0].user)).
to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
it "displays the current user in front regardless of position in the list" do
expect(award_user_list(awards, awards[12].user)).
to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
end
describe '#award_active_class' do describe '#award_active_class' do
let!(:upvote) { create(:award_emoji) } let!(:upvote) { create(:award_emoji) }
......
/*= require abuse_reports */
/*= require jquery */
((global) => {
const FIXTURE = 'abuse_reports.html';
const MAX_MESSAGE_LENGTH = 500;
function assertMaxLength($message) {
expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
}
describe('Abuse Reports', function() {
fixture.preload(FIXTURE);
beforeEach(function() {
fixture.load(FIXTURE);
new global.AbuseReports();
});
it('should truncate long messages', function() {
const $longMessage = $('#long');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
assertMaxLength($longMessage);
});
it('should not truncate short messages', function() {
const $shortMessage = $('#short');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', function() {
const $longMessage = $('#long');
$longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click();
assertMaxLength($longMessage);
});
});
})(window.gl);
...@@ -143,6 +143,52 @@ ...@@ -143,6 +143,52 @@
return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
}); });
}); });
describe('::addYouToUserList', function() {
it('should prepend "You" to the award tooltip', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
});
});
describe('::removeYouToUserList', function() {
it('removes "You" from the front of the tooltip', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
});
return it('handles the special case where "You" is not cleanly comma seperated', function() {
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
describe('search', function() { describe('search', function() {
return it('should filter the emoji', function() { return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click(); $('.js-add-award').eq(0).click();
......
.abuse-reports
.message#long
Cat ipsum dolor sit amet, hide head under blanket so no one can see.
Gate keepers of hell eat and than sleep on your face but hunt by meowing
loudly at 5am next to human slave food dispenser cats go for world
domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
cat is life chase after silly colored fish toys around the house climb a
tree, wait for a fireman jump to fireman then scratch his face fall asleep
on the washing machine lies down always hungry so caticus cuteicus. Sit on
human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
blanket so no one can see throwup on your pillow.
.message#short
Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
beauty sleep 18 hours - checked, be fabulous for the rest of the day -
checked! for shake treat bag.
...@@ -407,4 +407,27 @@ describe API::API, api: true do ...@@ -407,4 +407,27 @@ describe API::API, api: true do
end end
end end
end end
describe 'POST /projects/:id/builds/:build_id/play' do
before do
post api("/projects/#{project.id}/builds/#{build.id}/play", user)
end
context 'on an playable build' do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
it 'plays the build' do
expect(response).to have_http_status 200
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(build.id)
end
end
context 'on a non-playable build' do
it 'returns a status code 400, Bad Request' do
expect(response).to have_http_status 400
expect(response.body).to match("Unplayable Build")
end
end
end
end end
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { deployment.environment.project }
let!(:deployment) { create(:deployment) }
before do
project.team << [user, :master]
end
describe 'GET /projects/:id/deployments' do
context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do
get api("/projects/#{project.id}/deployments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['iid']).to eq(deployment.iid)
expect(json_response.first['sha']).to match /\A\h{40}\z/
end
end
context 'as non member' do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments", non_member)
expect(response).to have_http_status(404)
end
end
end
describe 'GET /projects/:id/deployments/:deployment_id' do
context 'as a member of the project' do
it 'returns the projects deployment' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
expect(json_response['id']).to eq(deployment.id)
end
end
context 'as non member' do
it 'returns a 404 status code' do
get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
expect(response).to have_http_status(404)
end
end
end
end
...@@ -26,6 +26,7 @@ describe API::API, api: true do ...@@ -26,6 +26,7 @@ describe API::API, api: true do
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project']['id']).to eq(project.id)
end end
end end
......
require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
before { project.team << [user, :master] }
describe 'GET /projects/:id/pipelines ' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/pipelines", user) }
end
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id
end
end
context 'unauthorized user' do
it 'does not return project pipelines' do
get api("/projects/#{project.id}/pipelines", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
end
end
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_http_status(200)
expect(json_response['sha']).to match /\A\h{40}\z/
end
it 'returns 404 when it does not exist' do
get api("/projects/#{project.id}/pipelines/123456", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Not found'
expect(json_response['id']).to be nil
end
end
context 'unauthorized user' do
it 'should not return a project pipeline' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
end
end
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
let!(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
it 'retries failed builds' do
expect do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
expect(response).to have_http_status(201)
expect(build.reload.retried?).to be true
end
end
context 'unauthorized user' do
it 'should not return a project pipeline' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response['id']).to be nil
end
end
end
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
let!(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'authorized user' do
it 'retries failed builds' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
expect(response).to have_http_status(200)
expect(json_response['status']).to eq('canceled')
end
end
context 'user without proper access rights' do
let!(:reporter) { create(:user) }
before { project.team << [reporter, :reporter] }
it 'rejects the action' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
expect(response).to have_http_status(403)
expect(pipeline.reload.status).to eq('pending')
end
end
end
end
...@@ -194,12 +194,12 @@ describe TodoService, services: true do ...@@ -194,12 +194,12 @@ describe TodoService, services: true do
end end
end end
describe '#mark_todos_as_done' do shared_examples 'marking todos as done' do |meth|
it 'marks related todos for the user as done' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
service.mark_todos_as_done([first_todo, second_todo], john_doe) it 'marks related todos for the user as done' do
service.send(meth, collection, john_doe)
expect(first_todo.reload).to be_done expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done expect(second_todo.reload).to be_done
...@@ -207,20 +207,30 @@ describe TodoService, services: true do ...@@ -207,20 +207,30 @@ describe TodoService, services: true do
describe 'cached counts' do describe 'cached counts' do
it 'updates when todos change' do it 'updates when todos change' do
todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
expect(john_doe.todos_done_count).to eq(0) expect(john_doe.todos_done_count).to eq(0)
expect(john_doe.todos_pending_count).to eq(1) expect(john_doe.todos_pending_count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.mark_todos_as_done([todo], john_doe) service.send(meth, collection, john_doe)
expect(john_doe.todos_done_count).to eq(1) expect(john_doe.todos_done_count).to eq(2)
expect(john_doe.todos_pending_count).to eq(0) expect(john_doe.todos_pending_count).to eq(0)
end end
end end
end end
describe '#mark_todos_as_done' do
it_behaves_like 'marking todos as done', :mark_todos_as_done do
let(:collection) { [first_todo, second_todo] }
end
end
describe '#mark_todos_as_done_by_ids' do
it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
let(:collection) { [first_todo, second_todo].map(&:id) }
end
end
describe '#new_note' do describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
......
class FakeU2fDevice class FakeU2fDevice
def initialize(page) attr_reader :name
def initialize(page, name)
@page = page @page = page
@name = name
end end
def respond_to_u2f_registration def respond_to_u2f_registration
......
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