diff options
44 files changed, 508 insertions, 187 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5b101ad6b..c039335c46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -182,6 +182,12 @@ entry. - Remove deprecated GitlabCiService. - Requeue pending deletion projects. +## 8.16.7 (2017-02-27) + +- No changes. +- No changes. +- Fix MR changes tab size count when there are over 100 files in the diff. + ## 8.16.6 (2017-02-17) - API: Fix file downloading. !0 (8267) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 53d8d313e39..c51860d1604 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,8 +7,6 @@ /* global Aside */ window.$ = window.jQuery = require('jquery'); -require('jquery-ui/ui/draggable'); -require('jquery-ui/ui/sortable'); require('jquery-ujs'); require('vendor/jquery.endless-scroll'); require('vendor/jquery.highlight'); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 8fa1aceddff..6e6e9b18686 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -7,7 +7,7 @@ var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { - Build.interval = null; + Build.timeout = null; Build.state = null; @@ -31,7 +31,7 @@ this.$scrollBottomBtn = $('#scroll-bottom'); this.$buildRefreshAnimation = $('.js-build-refresh'); - clearInterval(Build.interval); + clearTimeout(Build.timeout); // Init breakpoint checker this.bp = Breakpoints.get(); @@ -52,17 +52,7 @@ this.getInitialBuildTrace(); this.initScrollButtonAffix(); } - if (this.buildStatus === "running" || this.buildStatus === "pending") { - Build.interval = setInterval((function(_this) { - // Check for new build output if user still watching build page - // Only valid for runnig build when output changes during time - return function() { - if (_this.location() === _this.pageUrl) { - return _this.getBuildTrace(); - } - }; - })(this), 4000); - } + this.invokeBuildTrace(); } Build.prototype.initSidebar = function() { @@ -75,6 +65,22 @@ return window.location.href.split("#")[0]; }; + Build.prototype.invokeBuildTrace = function() { + var continueRefreshStatuses = ['running', 'pending']; + // Continue to update build trace when build is running or pending + if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) { + // Check for new build output if user still watching build page + // Only valid for runnig build when output changes during time + Build.timeout = setTimeout((function(_this) { + return function() { + if (_this.location() === _this.pageUrl) { + return _this.getBuildTrace(); + } + }; + })(this), 4000); + } + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; @@ -86,7 +92,7 @@ if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } - if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { + if (removeRefreshStatuses.indexOf(buildData.status) !== -1) { this.$buildRefreshAnimation.remove(); return this.initScrollMonitor(); } @@ -105,6 +111,7 @@ if (log.state) { _this.state = log.state; } + _this.invokeBuildTrace(); if (log.status === "running") { if (log.append) { $('.js-build-output').append(log.html); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 49bb64a3472..17d14dc1e79 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -52,6 +52,30 @@ return this.views[viewMode].call(this); }; + ImageFile.prototype.initDraggable = function($el, padding, callback) { + var dragging = false; + var $body = $('body'); + var $offsetEl = $el.parent(); + + $el.off('mousedown').on('mousedown', function() { + dragging = true; + $body.css('user-select', 'none'); + }); + + $body.off('mouseup').off('mousemove').on('mouseup', function() { + dragging = false; + $body.css('user-select', ''); + }) + .on('mousemove', function(e) { + var left; + if (!dragging) return; + + left = e.pageX - ($offsetEl.offset().left + padding); + + callback(e, left); + }); + }; + prepareFrames = function(view) { var maxHeight, maxWidth; maxWidth = 0; @@ -96,26 +120,30 @@ maxHeight = 0; return $('.swipe.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.swipe-frame', view).css({ + $swipeFrame = $('.swipe-frame', view); + $swipeWrap = $('.swipe-wrap', view); + $swipeBar = $('.swipe-bar', view); + + $swipeFrame.css({ width: maxWidth + 16, height: maxHeight + 28 }); - $('.swipe-wrap', view).css({ + $swipeWrap.css({ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.swipe-bar', view).css({ + $swipeBar.css({ left: 0 - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); - }, - stop: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + }); + + wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); + + _this.initDraggable($swipeBar, wrapPadding, function(e, left) { + if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) { + $swipeWrap.width((maxWidth + 1) - left); + $swipeBar.css('left', left); } }); }; @@ -128,9 +156,14 @@ dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); return $('.onion-skin.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.onion-skin-frame', view).css({ + $frame = $('.onion-skin-frame', view); + $frameAdded = $('.frame.added', view); + $track = $('.drag-track', view); + $dragger = $('.dragger', $track); + + $frame.css({ width: maxWidth + 16, height: maxHeight + 28 }); @@ -138,16 +171,18 @@ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.dragger', view).css({ + $dragger.css({ left: dragTrackWidth - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); - }, - stop: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + }); + + framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); + + _this.initDraggable($dragger, framePadding, function(e, left) { + var opacity = left / dragTrackWidth; + + if (opacity >= 0 && opacity <= 1) { + $dragger.css('left', left); + $frameAdded.css('opacity', opacity); } }); }; diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 6a91982ffa7..08579d0e826 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -486,25 +486,23 @@ module.exports = Vue.component('environment-item', { </span> </td> - <td class="hidden-xs"> - <div v-if="!model.isFolder"> - <div class="btn-group" role="group"> - <actions-component v-if="hasManualActions && canCreateDeployment" - :actions="manualActions"/> + <td class="hidden-xs environments-actions"> + <div v-if="!model.isFolder" class="btn-group" role="group"> + <actions-component v-if="hasManualActions && canCreateDeployment" + :actions="manualActions"/> - <external-url-component v-if="externalURL && canReadEnvironment" - :external-url="externalURL"/> + <external-url-component v-if="externalURL && canReadEnvironment" + :external-url="externalURL"/> - <stop-component v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path"/> + <stop-component v-if="hasStopAction && canCreateDeployment" + :stop-url="model.stop_path"/> - <terminal-button-component v-if="model && model.terminal_path" - :terminal-path="model.terminal_path"/> + <terminal-button-component v-if="model && model.terminal_path" + :terminal-path="model.terminal_path"/> - <rollback-component v-if="canRetry && canCreateDeployment" - :is-last-deployment="isLastDeployment" - :retry-url="retryUrl"/> - </div> + <rollback-component v-if="canRetry && canCreateDeployment" + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"/> </div> </td> </tr> diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index fbc72a3001a..dd565da507e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -48,7 +48,11 @@ } setOffset(offset = 0) { - this.dropdown.style.left = `${offset}px`; + if (window.innerWidth > 480) { + this.dropdown.style.left = `${offset}px`; + } else { + this.dropdown.style.left = '0px'; + } } renderContent(forceShowList = false) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index a01662e2f9e..9e6ed06054b 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -63,7 +63,7 @@ } GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) >= 0; + return BLUR_KEYCODES.indexOf(keyCode) !== -1; }; GitLabDropdownFilter.prototype.filter = function(search_text) { @@ -605,7 +605,7 @@ var occurrences; occurrences = fuzzaldrinPlus.match(text, term); return text.split('').map(function(character, i) { - if (indexOf.call(occurrences, i) >= 0) { + if (indexOf.call(occurrences, i) !== -1) { return "<b>" + character + "</b>"; } else { return character; @@ -748,7 +748,7 @@ return function(e) { var $listItems, PREV_INDEX, currentKeyCode; currentKeyCode = e.which; - if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { + if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { e.preventDefault(); e.stopImmediatePropagation(); PREV_INDEX = currentIndex; diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 8df86f68218..3bfce32768a 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -116,7 +116,7 @@ formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; - issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); + issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); issuesUrl += formData; return gl.utils.visitUrl(issuesUrl); }; diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 88f08bbaa34..00c6c050612 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -83,7 +83,7 @@ require('./smart_interval'); return function() { var page; page = $('body').data('page').split(':').last(); - if (allowedPages.indexOf(page) < 0) { + if (allowedPages.indexOf(page) === -1) { return _this.clearEventListeners(); } }; @@ -233,7 +233,7 @@ require('./smart_interval'); } $('.ci_widget').hide(); allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; - if (indexOf.call(allowed_states, state) >= 0) { + if (indexOf.call(allowed_states, state) !== -1) { $('.ci_widget.ci-' + state).show(); switch (state) { case "failed": diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 3f678b93f73..5828f460a23 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -81,7 +81,7 @@ var errorMessage, errors, formatter, unique, validator; this.branchNameError.empty(); unique = function(values, value) { - if (indexOf.call(values, value) < 0) { + if (indexOf.call(values, value) === -1) { values.push(value); } return values; diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7c03c8b72d4..db7ceaa2421 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -116,7 +116,7 @@ if ($('input[name="ref"]').length) { var $form = $dropdown.closest('form'); var action = $form.attr('action'); - var divider = action.indexOf('?') < 0 ? '?' : '&'; + var divider = action.indexOf('?') === -1 ? '?' : '&'; gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e3da467a27c..d2be8dc7a39 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -26,6 +26,11 @@ .filtered-search-container { display: -webkit-flex; display: flex; + + @media (max-width: $screen-xs-min) { + -webkit-flex-direction: column; + flex-direction: column; + } } .filtered-search-input-container { @@ -34,6 +39,20 @@ position: relative; width: 100%; + @media (max-width: $screen-xs-min) { + -webkit-flex: 1 1 100%; + flex: 1 1 100%; + margin-bottom: 10px; + + .dropdown-menu { + width: auto; + left: 0; + right: 0; + max-width: none; + min-width: 100%; + } + } + .form-control { padding-left: 25px; padding-right: 25px; @@ -79,6 +98,31 @@ overflow: auto; } +@media (max-width: $screen-xs-min) { + .issues-details-filters { + padding: 0 0 10px; + background-color: $white-light; + border-top: 0; + } + + .filter-dropdown-container { + .dropdown-toggle, + .dropdown { + width: 100%; + } + + .dropdown { + margin-left: 0; + } + + .fa-chevron-down { + position: absolute; + right: 10px; + top: 10px; + } + } +} + %filter-dropdown-item-btn-hover { background-color: $dropdown-hover-color; color: $white-light; @@ -148,4 +192,4 @@ .filter-dropdown-loading { padding: 8px 16px; -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 08b3206f31e..f4707f71208 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -128,6 +128,10 @@ padding: 10px 8px; } + td.environments-actions { + padding-right: 0; + } + td.stage-cell { padding: 10px 0; } diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index ff937b5ebd2..5ac3e66bb1f 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -15,4 +15,11 @@ module BuildsHelper log_state: @build.trace_with_state[:state].to_s } end + + def build_failed_issue_options + { + title: "Build Failed ##{@build.id}", + description: namespace_project_build_url(@project.namespace, @project, @build) + } + end end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 86f317dcd18..e84944ed411 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader File.join(self.class.artifacts_cache_path, @build.artifacts_path) end - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def filename file.try(:filename) end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index cfcb877cc3e..6aa1f5a8c50 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 265cea2d2c6..b4c393c6f2c 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def exists? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 23b7318827c..0d2edaeff3b 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -4,15 +4,12 @@ class FileUploader < GitlabUploader storage :file - attr_accessor :project, :secret + attr_accessor :project + attr_reader :secret def initialize(project, secret = nil) @project = project - @secret = secret || self.class.generate_secret - end - - def base_dir - "uploads" + @secret = secret || generate_secret end def store_dir @@ -23,10 +20,6 @@ class FileUploader < GitlabUploader File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def secure_url - File.join("/uploads", @secret, file.filename) - end - def to_markdown to_h[:markdown] end @@ -35,17 +28,23 @@ class FileUploader < GitlabUploader filename = image_or_video? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") - markdown = "[#{escaped_filename}](#{self.secure_url})" + markdown = "[#{escaped_filename}](#{secure_url})" markdown.prepend("!") if image_or_video? || dangerous? { alt: filename, - url: self.secure_url, + url: secure_url, markdown: markdown } end - def self.generate_secret + private + + def generate_secret SecureRandom.hex end + + def secure_url + File.join('/uploads', @secret, file.filename) + end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 02d7c601d6c..bd7de4ed562 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -1,4 +1,14 @@ class GitlabUploader < CarrierWave::Uploader::Base + def self.base_dir + 'uploads' + end + + delegate :base_dir, to: :class + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + # Reduce disk IO def move_to_cache true diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index bee311583ea..7635c20ab3a 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -27,6 +27,8 @@ module UploaderHelper extension_match?(DANGEROUS_EXT) end + private + def extension_match?(extensions) return false unless file @@ -40,8 +42,4 @@ module UploaderHelper extensions.include?(extension.downcase) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 27e81c2bec3..7eb17e887e7 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,4 +1,4 @@ -.content-block.build-header +.content-block.build-header.top-area .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false Job @@ -16,7 +16,10 @@ - if @build.user = render "user" = time_ago_with_tooltip(@build.created_at) - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + .nav-controls + - if can?(current_user, :create_issue, @project) && @build.failed? + = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8e04b50bb8a..62f09cc2dc1 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -82,7 +82,7 @@ %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right + .pull-right.filter-dropdown-container = render 'shared/sort_dropdown' - if @bulk_edit diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml new file mode 100644 index 00000000000..580d1074aa7 --- /dev/null +++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml @@ -0,0 +1,4 @@ +--- +title: Add button to create issue for failing build +merge_request: 9391 +author: Alex Sanford diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml new file mode 100644 index 00000000000..87b1f0c5572 --- /dev/null +++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml @@ -0,0 +1,4 @@ +--- +title: Enhanced filter issues layout for better mobile experiance +merge_request: 9280 +author: Pratik Borsadiya diff --git a/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml b/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml new file mode 100644 index 00000000000..800e0389c86 --- /dev/null +++ b/changelogs/unreleased/28212-avoid-dos-on-build-trace.yml @@ -0,0 +1,4 @@ +--- +title: Replace setInterval with setTimeout to prevent highly frequent requests +merge_request: 9271 +author: Takuya Noguchi diff --git a/changelogs/unreleased/28723-consistent-handling-indexof.yml b/changelogs/unreleased/28723-consistent-handling-indexof.yml new file mode 100644 index 00000000000..95d6181d5fa --- /dev/null +++ b/changelogs/unreleased/28723-consistent-handling-indexof.yml @@ -0,0 +1,4 @@ +--- +title: Keep consistent in handling indexOf results +merge_request: 9531 +author: Takuya Noguchi diff --git a/changelogs/unreleased/fix-mr-size-with-over-100-files.yml b/changelogs/unreleased/fix-mr-size-with-over-100-files.yml deleted file mode 100644 index eecf3c99a75..00000000000 --- a/changelogs/unreleased/fix-mr-size-with-over-100-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix MR changes tab size count when there are over 100 files in the diff -merge_request: -author: diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/19_nested_groups.rb new file mode 100644 index 00000000000..d8dddc3fee9 --- /dev/null +++ b/db/fixtures/development/19_nested_groups.rb @@ -0,0 +1,69 @@ +require './spec/support/sidekiq' + +def create_group_with_parents(user, full_path) + parent_path = nil + group = nil + + until full_path.blank? + path, _, full_path = full_path.partition('/') + + if parent_path + parent = Group.find_by_full_path(parent_path) + + parent_path += '/' + parent_path += path + + group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute + else + parent_path = path + + group = Group.find_by_full_path(parent_path) || + Groups::CreateService.new(user, path: path).execute + end + end + + group +end + +Sidekiq::Testing.inline! do + Gitlab::Seeder.quiet do + project_urls = [ + 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git', + 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git', + 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git', + 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git', + 'https://android.googlesource.com/platform/hardware/bsp/freescale.git', + 'https://android.googlesource.com/platform/hardware/bsp/imagination.git', + 'https://android.googlesource.com/platform/hardware/bsp/intel.git', + 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git', + 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git' + ] + + user = User.admins.first + + project_urls.each_with_index do |url, i| + full_path = url.sub('https://android.googlesource.com/', '') + full_path = full_path.sub(/\.git\z/, '') + full_path, _, project_path = full_path.rpartition('/') + group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path) + + params = { + import_url: url, + namespace_id: group.id, + path: project_path, + name: project_path, + description: FFaker::Lorem.sentence, + visibility_level: Gitlab::VisibilityLevel.values.sample + } + + project = Projects::CreateService.new(user, params).execute + project.send(:_run_after_commit_queue) + + if project.valid? + print '.' + else + print 'F' + end + end + end +end diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb index ce4f00e25fa..cd8b505de9f 100644 --- a/db/migrate/20160610201627_migrate_users_notification_level.rb +++ b/db/migrate/20160610201627_migrate_users_notification_level.rb @@ -1,4 +1,6 @@ class MigrateUsersNotificationLevel < ActiveRecord::Migration + DOWNTIME = false + # Migrates only users who changed their default notification level :participating # creating a new record on notification settings table diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index f0fb6084a35..651b55523c0 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -8,11 +8,6 @@ module Banzai # of the anchor, and then replace the img with the link-wrapped version. def call doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| - div = doc.document.create_element( - 'div', - class: 'image-container' - ) - link = doc.document.create_element( 'a', class: 'no-attachment-icon', @@ -22,9 +17,7 @@ module Banzai link.children = img.clone - div.children = link - - img.replace(div) + img.replace(link) end doc diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb index 3fe32adeade..6105d165810 100644 --- a/lib/gitlab/middleware/webpack_proxy.rb +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -8,16 +8,16 @@ module Gitlab @proxy_host = opts.fetch(:proxy_host, 'localhost') @proxy_port = opts.fetch(:proxy_port, 3808) @proxy_path = opts[:proxy_path] if opts[:proxy_path] - super(app, opts) + + super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts) end def perform_request(env) - unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") - return @app.call(env) + if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + super(env) + else + @app.call(env) end - - env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}" - super(env) end end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index f8c3ccb416b..b740e191f48 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/ + expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 1e0db4a0499..1c8267b1593 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include DropzoneHelper include IssueHelpers include SortingHelper include WaitForAjax @@ -570,19 +571,13 @@ describe 'Issues', feature: true do end it 'uploads file when dragging into textarea' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to have_content 'banana_sample' end it 'adds double newline to end of attachment markdown' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to match /\n\n$/ end @@ -665,25 +660,4 @@ describe 'Issues', feature: true do end end end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end end diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb new file mode 100644 index 00000000000..f88a515f7fc --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +feature 'User uploads avatar to group', feature: true do + scenario 'they see the new avatar' do + user = create(:user) + group = create(:group) + group.add_owner(user) + login_as(user) + + visit edit_group_path(group) + attach_file( + 'group_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Save group' + + visit group_path(group) + + expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(group.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb new file mode 100644 index 00000000000..0dfd29045e5 --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +feature 'User uploads avatar to profile', feature: true do + scenario 'they see their new avatar' do + user = create(:user) + login_as(user) + + visit profile_path + attach_file( + 'user_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Update profile settings' + + visit user_path(user) + + expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(user.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb new file mode 100644 index 00000000000..0c160dd74b4 --- /dev/null +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'User uploads file to note', feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user, namespace: user.namespace) } + + scenario 'they see the attached file', js: true do + issue = create(:issue, project: project, author: user) + + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + + dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Comment' + wait_for_ajax + + expect(find('a.no-attachment-icon img[alt="dk"]')['src']) + .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + end +end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index a2a1ed58d1b..294558b3db2 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do end it 'does not wrap a duplicate link' do - exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>) - expect(filter(act).to_html).to eq exp + doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)) + expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/ end it 'works with external images' do @@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] end - it 'wraps the image with a link and a div' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.to_html).to include('<div class="image-container">') + it 'works with inline images' do + doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) + expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/ end end diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb new file mode 100644 index 00000000000..984ec7d2741 --- /dev/null +++ b/spec/support/dropzone_helper.rb @@ -0,0 +1,37 @@ +module DropzoneHelper + # Provides a way to perform `attach_file` for a Dropzone-based file input + # + # This is accomplished by creating a standard HTML file input on the page, + # performing `attach_file` on that field, and then triggering the appropriate + # Dropzone events to perform the actual upload. + # + # This method waits for the upload to complete before returning. + def dropzone_file(file_path) + # Generate a fake file input that Capybara can attach to + page.execute_script <<-JS.strip_heredoc + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + + window._dropzoneComplete = false; + JS + + # Attach the file to the fake input selector with Capybara + attach_file('fakeFileInput', file_path) + + # Manually trigger a Dropzone "drop" event with the fake input's file list + page.execute_script <<-JS.strip_heredoc + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + + var dropzone = $('.div-dropzone')[0].dropzone; + dropzone.on('queuecomplete', function() { + window._dropzoneComplete = true; + }); + dropzone.listeners[0].events.drop(e); + JS + + # Wait until Dropzone's fired `queuecomplete` + loop until page.evaluate_script('window._dropzoneComplete === true') + end +end diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb index f984ac7bfa7..365c34448ac 100644 --- a/spec/support/update_invalid_issuable.rb +++ b/spec/support/update_invalid_issuable.rb @@ -33,7 +33,7 @@ shared_examples 'update invalid issuable' do |klass| end it 'renders json error message when format is json' do - params.merge!(format: "json") + params[:format] = "json" put :update, params diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index 6098be5cd45..ea714fb08f0 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AttachmentUploader do - let(:issue) { build(:issue) } - subject { described_class.new(issue) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is true' do - expect(subject.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(subject.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index 76f5a4b42ed..c4d558805ab 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AvatarUploader do - let(:user) { build(:user) } - subject { described_class.new(user) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is false' do - expect(subject.move_to_cache).to eq(false) + expect(uploader.move_to_cache).to eq(false) end end describe '#move_to_store' do it 'is false' do - expect(subject.move_to_store).to eq(false) + expect(uploader.move_to_store).to eq(false) end end end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 6a712e33c96..b0f5be55c33 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,57 +1,35 @@ require 'spec_helper' describe FileUploader do - let(:project) { create(:project) } + let(:uploader) { described_class.new(build_stubbed(:project)) } - before do - @previous_enable_processing = FileUploader.enable_processing - FileUploader.enable_processing = false - @uploader = FileUploader.new(project) - end - - after do - FileUploader.enable_processing = @previous_enable_processing - @uploader.remove! - end + describe 'initialize' do + it 'generates a secret if none is provided' do + expect(SecureRandom).to receive(:hex).and_return('secret') - describe '#image_or_video?' do - context 'given an image file' do - before do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) - end + uploader = described_class.new(double) - it 'detects an image based on file extension' do - expect(@uploader.image_or_video?).to be true - end + expect(uploader.secret).to eq 'secret' end - context 'given an video file' do - before do - video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) - @uploader.store!(video_file) - end - - it 'detects a video based on file extension' do - expect(@uploader.image_or_video?).to be true - end - end + it 'accepts a secret parameter' do + expect(SecureRandom).not_to receive(:hex) - it 'does not return image_or_video? for other types' do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + uploader = described_class.new(double, 'secret') - expect(@uploader.image_or_video?).to be false + expect(uploader.secret).to eq 'secret' end end describe '#move_to_cache' do it 'is true' do - expect(@uploader.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(@uploader.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb new file mode 100644 index 00000000000..e9efd13b9aa --- /dev/null +++ b/spec/uploaders/uploader_helper_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe UploaderHelper do + class ExampleUploader < CarrierWave::Uploader::Base + include UploaderHelper + + storage :file + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + describe '#image_or_video?' do + let(:uploader) { ExampleUploader.new } + + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).to be_image_or_video + end + + it 'it returns true for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).to be_image_or_video + end + + it 'returns false for other extensions' do + uploader.store!(upload_fixture('doc_sample.txt')) + + expect(uploader).not_to be_image_or_video + end + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index b6f6e7b7a2b..ec78ac30593 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do it 'does not show retry button' do expect(rendered).not_to have_link('Retry') end + + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end end context 'when job is not running' do @@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do it 'shows retry button' do expect(rendered).to have_link('Retry') end + + context 'if build passed' do + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end + end + + context 'if build failed' do + before do + build.status = 'failed' + render + end + + it 'shows New issue button' do + expect(rendered).to have_link('New issue') + end + end end describe 'commit title in sidebar' do @@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end + + describe 'New issue button' do + before do + build.status = 'failed' + render + end + + it 'links to issues/new with the title and description filled in' do + title = "Build Failed ##{build.id}" + build_url = namespace_project_build_url(project.namespace, project, build) + href = new_namespace_project_issue_path( + project.namespace, + project, + issue: { + title: title, + description: build_url + } + ) + expect(rendered).to have_link('New issue', href: href) + end + end end |