summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/javascripts/boards/models/label.js.es61
-rw-r--r--app/assets/javascripts/gl_dropdown.js74
-rw-r--r--app/assets/javascripts/search_autocomplete.js18
-rw-r--r--app/controllers/projects/boards/issues_controller.rb2
-rw-r--r--app/helpers/time_helper.rb17
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/commit_status.rb13
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/concerns/statuseable.rb15
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/legacy_diff_note.rb4
-rw-r--r--app/views/admin/builds/_build.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/workers/emails_on_push_worker.rb4
-rw-r--r--lib/extracts_path.rb12
-rw-r--r--lib/gitlab/utils.rb2
-rw-r--r--spec/fixtures/api/schemas/issue.json40
-rw-r--r--spec/helpers/time_helper_spec.rb16
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/search_autocomplete_spec.js8
-rw-r--r--spec/lib/extracts_path_spec.rb21
-rw-r--r--spec/models/broadcast_message_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb14
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb46
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb36
34 files changed, 381 insertions, 139 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 1bb9409bb2d..84cce36a910 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -31,6 +31,7 @@ v 8.11.0 (unreleased)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
- Fix commit mention font inconsistency (ClemMakesApps)
+ - Do not escape URI when extracting path !5878 (winniehell)
- Fix filter label tooltip HTML rendering (ClemMakesApps)
- Cache the commit author in RequestStore to avoid extra lookups in PostReceive
- Expand commit message width in repo view (ClemMakesApps)
@@ -40,6 +41,7 @@ v 8.11.0 (unreleased)
- API: Add deployment endpoints
- API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+ - Show wall clock time when showing a pipeline. !5734
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index e81e91fe972..583829552cd 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -3,6 +3,7 @@ class ListLabel {
this.id = obj.id;
this.title = obj.title;
this.color = obj.color;
+ this.textColor = obj.text_color;
this.description = obj.description;
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d3394fae3f9..f85aa6dd7b7 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -31,9 +31,8 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
-
if (keyCode === 13) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('keyup', function(e) {
@@ -111,9 +110,9 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
- return $el.show();
+ return $el.show().removeClass('option-hidden');
} else {
- return $el.hide();
+ return $el.hide().addClass('option-hidden');
}
}
});
@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
@@ -191,6 +190,12 @@
currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
+
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5
+
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
+ currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
@@ -240,7 +246,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
- selector = '.dropdown-content li:not(.divider)';
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -256,7 +262,7 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
- selector = '.dropdown-content li:not(.divider):visible';
+ selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -376,7 +382,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -387,7 +393,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
- currentIndex = -1;
+ this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
@@ -410,6 +416,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
+ this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
@@ -463,7 +470,7 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
- return "<li class='dropdown-header'>" + data.header + "</li>";
+ return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
@@ -494,11 +501,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
- groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
+ groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
- html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+ html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+ url: url,
+ groupAttrs: groupAttrs,
+ cssClass: cssClass,
+ text: text
+ });
}
return html;
};
@@ -520,17 +532,6 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
- GitLabDropdown.prototype.highlightRow = function(index) {
- var selector;
- if (this.filterInput.val() !== "") {
- selector = '.dropdown-content li:first-child a';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content li:first-child a";
- }
- return this.getElement(selector).addClass('is-focused');
- }
- };
-
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
fieldName = this.options.fieldName;
@@ -609,13 +610,15 @@
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
- selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
- return $el.first().trigger('click');
+ $el.first().trigger('click');
+ var href = $el.attr('href');
+ if (href && href !== '#') Turbolinks.visit(href);
}
};
@@ -623,7 +626,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
- selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
+ selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
@@ -651,7 +654,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
- return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
+ return _this.selectRowAtIndex(currentIndex);
}
};
})(this));
@@ -661,6 +664,11 @@
return $('body').off('keydown');
};
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
+
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
@@ -674,10 +682,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
- if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
+ if (!index) {
+ $dropdownContent.scrollTop(0)
+ } else if (index === ($listItems.length - 1)) {
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990f6536eb2..227e8c696b4 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
- ENTER: 13
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
};
function SearchAutocomplete(opts) {
@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open');
- return this.restoreMenu();
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
};
SearchAutocomplete.prototype.restoreMenu = function() {
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 2d894b3dd4a..1a4f6b50e8f 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -12,7 +12,7 @@ module Projects
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority] }
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 790001222f1..271e839692a 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -15,20 +15,9 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(finished_at, started_at)
- interval = interval_in_seconds(started_at, finished_at)
- time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration)
+ time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
- Time.at(interval).utc.strftime(time_format)
- end
-
- private
-
- def interval_in_seconds(started_at, finished_at = nil)
- if started_at && finished_at
- finished_at.to_i - started_at.to_i
- elsif started_at
- Time.now.to_i - started_at.to_i
- end
+ Time.at(duration).utc.strftime(time_format)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ed056a07a49..096b3b801af 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -62,6 +62,7 @@ module Ci
status_event: 'enqueue'
)
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c360a6ff729..087abe4cbb1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -78,6 +78,10 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages
end
+ def self.total_duration
+ where.not(duration: nil).sum(:duration)
+ end
+
def stages_with_latest_statuses
statuses.latest.order(:stage_idx).group_by(&:stage)
end
@@ -146,6 +150,10 @@ module Ci
end
end
+ def mark_as_processable_after_stage(stage_idx)
+ builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ end
+
def latest?
return false unless ref
commit = project.commit(ref)
@@ -250,7 +258,7 @@ module Ci
end
def update_duration
- self.duration = statuses.latest.duration
+ self.duration = calculate_duration
end
def execute_hooks
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 703ca90edb6..84ceeac7d3e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -21,6 +21,7 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
+
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
@@ -30,6 +31,10 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :skipped] => :pending
end
+ event :process do
+ transition skipped: :created
+ end
+
event :run do
transition pending: :running
end
@@ -107,13 +112,7 @@ class CommitStatus < ActiveRecord::Base
end
def duration
- duration =
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
- duration
+ calculate_duration
end
def stuck?
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 4be6a2f621b..a881fb83b7f 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -17,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
+ def original_line_code
+ raise NotImplementedError
+ end
+
def diff_attributes
raise NotImplementedError
end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 5d4b0a86899..750f937b724 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -35,11 +35,6 @@ module Statuseable
all.pluck(self.status_sql).first
end
- def duration
- duration_array = all.map(&:duration).compact
- duration_array.reduce(:+)
- end
-
def started_at
all.minimum(:started_at)
end
@@ -85,4 +80,14 @@ module Statuseable
def complete?
COMPLETED_STATUSES.include?(status)
end
+
+ private
+
+ def calculate_duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index f56c3d74ae3..aa54189fea9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -57,6 +57,10 @@ class DiffNote < Note
diff_file.position(line) == self.original_position
end
+ def original_line_code
+ self.diff_file.line_code(self.diff_line)
+ end
+
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 3fddc084af2..9676bc03470 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -12,6 +12,7 @@ class Discussion
:for_merge_request?,
:line_code,
+ :original_line_code,
:diff_file,
:for_line?,
:active?,
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 8e26cbe9835..40277a9b139 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -49,6 +49,10 @@ class LegacyDiffNote < Note
!line.meta? && diff_file.line_code(line) == self.line_code
end
+ def original_line_code
+ self.line_code
+ end
+
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 352adbedee4..f29d9c94441 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -51,7 +51,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index b2e55f7647a..3a95a652810 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
.diff-content.code.js-syntax-highlight
%table
- - discussions = { discussion.line_code => discussion }
+ - discussions = { discussion.original_line_code => discussion }
= render partial: "projects/diffs/line",
collection: discussion.truncated_diff_lines,
as: :line,
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 91081435220..1fdf32466f2 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
= icon("calendar")
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index be387201f8d..9a672b23341 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -48,10 +48,10 @@
\-
%td
- - if pipeline.started_at && pipeline.finished_at
+ - if pipeline.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+ = duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 14adee7a6e9..29d767e7769 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -58,9 +58,8 @@
= ci_icon_for_status(@commit.status)
%span.ci-status-label
= ci_label_for_status(@commit.status)
- - if @commit.pipelines.duration
- in
- = time_interval_in_words @commit.pipelines.duration
+ in
+ = time_interval_in_words @commit.pipelines.total_duration
.commit-box.content-block
%h3.commit-title
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde7..063e83a407a 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,7 @@
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
- = time_interval_in_words @pipeline.duration
+ = time_interval_in_words(@pipeline.duration)
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index c6a5af2809a..1dc7e0adef7 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,13 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new.execute(project, after_sha, project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new.execute(project, before_sha, project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 84688f6646e..a293fa2752f 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,9 @@ module ExtractsPath
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
- @id = Addressable::URI.normalize_component(get_id)
+ @id = params[:id] || params[:ref]
+ @id += "/" + params[:path] unless params[:path].blank?
+
@ref, @path = extract_ref(@id)
@repo = @project.repository
if @options[:extended_sha1].blank?
@@ -116,12 +118,4 @@ module ExtractsPath
def tree
@tree ||= @repo.tree(@commit.id, @path)
end
-
- private
-
- def get_id
- id = params[:id] || params[:ref]
- id += "/" + params[:path] unless params[:path].blank?
- id
- end
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9..e59ead5d76c 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,7 +7,7 @@ module Gitlab
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
- Popen::popen(cmd).last.zero?
+ Popen.popen(cmd).last.zero?
end
def force_utf8(str)
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 299e4675d6f..532ebb9640e 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -10,23 +10,31 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"labels": {
- "type": ["array"],
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
},
- "description": { "type": ["string", "null"] },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
+ "additionalProperties": false
}
},
"assignee": {
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index bf3ed5c094c..21f35585367 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -19,16 +19,16 @@ describe TimeHelper do
describe "#duration_in_numbers" do
it "returns minutes and seconds" do
- duration_in_numbers = {
- [100, 0] => "01:40",
- [121, 0] => "02:01",
- [3721, 0] => "01:02:01",
- [0, 0] => "00:00",
- [nil, Time.now.to_i - 42] => "00:42"
+ durations_and_expectations = {
+ 100 => "01:40",
+ 121 => "02:01",
+ 3721 => "01:02:01",
+ 0 => "00:00",
+ 42 => "00:42"
}
- duration_in_numbers.each do |interval, expectation|
- expect(duration_in_numbers(*interval)).to eq(expectation)
+ durations_and_expectations.each do |duration, expectation|
+ expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d67..324f5152780 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe(' Issues assigned to me ');
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe(" Issues I've created ");
+ expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
- expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
- return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 36c77206a3f..86d04ecfa36 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -30,17 +30,6 @@ describe ExtractsPath, lib: true do
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
- context 'escaped slash character in ref' do
- let(:ref) { 'improve%2Fawesome' }
-
- it 'has no escape sequences in @ref or @logs_path' do
- assign_ref_vars
-
- expect(@ref).to eq('improve/awesome')
- expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
- end
- end
-
context 'ref contains %20' do
let(:ref) { 'foo%20bar' }
@@ -52,6 +41,16 @@ describe ExtractsPath, lib: true do
expect(@id).to start_with('foo%20bar/')
end
end
+
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+ it 'is not converted to %20 in @path' do
+ assign_ref_vars
+
+ expect(@path).to eq(params[:path])
+ end
+ end
end
describe '#extract_ref' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 72688137f08..02d6263094a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- include ActiveSupport::Testing::TimeHelpers
-
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8137e9f8f71..721b20e0cb2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -124,17 +124,21 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
- let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
+ let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
describe '#duration' do
before do
- build.skip
- build2.skip
+ travel_to(current - 120) do
+ pipeline.run
+ end
+
+ travel_to(current) do
+ pipeline.succeed
+ end
end
it 'matches sum of builds duration' do
- expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+ expect(pipeline.reload.duration).to eq(120)
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ad8c2485888..8326e5cd313 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do
let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
- let(:all_builds) { pipeline.builds }
- let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
let(:config) { nil }
before do
@@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do
end
describe '#execute' do
+ def all_builds
+ pipeline.builds
+ end
+
+ def builds
+ all_builds.where.not(status: [:created, :skipped])
+ end
+
def create_builds
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do
it 'does not process pipeline if existing stage is running' do
expect(create_builds).to be_truthy
expect(builds.pending.count).to eq(2)
-
+
expect(create_builds).to be_falsey
expect(builds.pending.count).to eq(2)
end
@@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do
end
end
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
+ end
+
+ it 'does trigger builds in the next stage' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+ expect(builds.pluck(:name)).to contain_exactly(
+ 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+ end
+ end
+ end
+
context 'creates a builds from .gitlab-ci.yml' do
let(:config) do
YAML.dump({
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2e2aa7c4fc0..c144cd85487 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -33,6 +33,7 @@ RSpec.configure do |config|
config.include EmailHelpers
config.include TestEnv
config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index eecc32875a5..7ca2c29da1c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,19 +2,19 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+ let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
context "when push is a new branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
@@ -31,8 +31,6 @@ describe EmailsOnPushWorker do
end
context "when push is a deleted branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
@@ -48,15 +46,40 @@ describe EmailsOnPushWorker do
end
end
- context "when there are no errors in sending" do
- let(:email) { ActionMailer::Base.deliveries.last }
+ context "when push is a force push to delete commits" do
+ before do
+ data_force_push = data.stringify_keys.merge(
+ "after" => data[:before],
+ "before" => data[:after]
+ )
+
+ subject.perform(project.id, recipients, data_force_push)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+ it "mentions force pushing in the body" do
+ expect(email).to have_body_text("force push")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
+ it "does not mention force pushing in the body" do
+ expect(email).not_to have_body_text("force push")
+ end
+
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
@@ -66,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(subject).to receive_message_chain(:logger, :info)
perform
end