diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d04069df885..195783454f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -331,7 +331,7 @@ trigger_docs: cache: {} artifacts: {} script: - - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds" + - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds" only: - master@gitlab-org/gitlab-ce diff --git a/CHANGELOG.md b/CHANGELOG.md index 9411cc62003..e60f526d925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,16 @@ entry. - Show correct environment log in admin/logs (@duk3luk3 !7191) - Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117 +- Diff collapse won't shift when collapsing. - Backups do not fail anymore when using tar on annex and custom_hooks only. !5814 - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - Trim leading and trailing whitespace on project_path (Linus Thiel) - Prevent award emoji via notes for issues/MRs authored by user (barthc) - Adds support for the `token` attribute in project hooks API (Gauvain Pocentek) +- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) - Fix Markdown styling inside reference links (Jan Zdráhal) +- Create new issue board list after creating a new label - Fix extra space on Build sidebar on Firefox !7060 - Fail gracefully when creating merge request with non-existing branch (alexsanford) - Fix mobile layout issues in admin user overview page !7087 @@ -66,8 +69,32 @@ entry. - In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo) - Improve search query parameter naming in /admin/users !7115 (YarNayar) - Fix table pagination to be responsive +- Fix applying GitHub-imported labels when importing job is interrupted - Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar) - Updated commit SHA styling on the branches page. +- Fix 404 when visit /projects page + +## 8.13.5 (2016-11-08) + +- Restore unauthenticated access to public container registries + +## 8.13.4 (2016-11-07) + +- Fix showing pipeline status for a given commit from correct branch. !7034 +- Only skip group when it's actually a group in the "Share with group" select. !7262 +- Introduce round-robin project creation to spread load over multiple shards. !7266 +- Ensure merge request's "remove branch" accessors return booleans. !7267 +- Ensure external users are not able to clone disabled repositories. +- Fix XSS issue in Markdown autolinker. +- Respect event visibility in Gitlab::ContributionsCalendar. +- Honour issue and merge request visibility in their respective finders. +- Disable reference Markdown for unavailable features. +- Fix lightweight tags not processed correctly by GitTagPushService. !6532 +- Allow owners to fetch source code in CI builds. !6943 +- Return conflict error in label API when title is taken by group label. !7014 +- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123 +- Fix builds tab visibility. !7178 +- Fix project features default values. !7181 ## 8.13.3 (2016-11-02) @@ -266,6 +293,10 @@ entry. - Fix broken Project API docs (Takuya Noguchi) - Migrate invalid project members (owner -> master) +## 8.12.9 (2016-11-07) + +- Fix XSS issue in Markdown autolinker + ## 8.12.8 (2016-11-02) - Removes any symlinks before importing a project export file. CVE-2016-9086 @@ -530,6 +561,10 @@ entry. - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs +## 8.11.11 (2016-11-07) + +- Fix XSS issue in Markdown autolinker + ## 8.11.10 (2016-11-02) - Removes any symlinks before importing a project export file. CVE-2016-9086 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 7ada0d303f3..3eefcb9dd5b 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.8.5 +1.0.0 diff --git a/Gemfile b/Gemfile index 78748d0e9f8..cb2a8470126 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-github', '~> 1.1.1' -gem 'omniauth-gitlab', '~> 1.0.0' +gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-saml', '~> 1.7.0' @@ -100,7 +100,7 @@ gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' -gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie' +gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'gitlab-markup', '~> 1.5.0' gem 'redcarpet', '~> 3.3.3' gem 'RedCloth', '~> 4.3.2' @@ -152,7 +152,7 @@ gem 'settingslogic', '~> 2.0.9' gem 'version_sorter', '~> 2.1.0' # Cache -gem 'redis-rails', '~> 4.0.0' +gem 'redis-rails', '~> 5.0.1' # Redis gem 'redis', '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 3ecff5f6a68..290e4c3e1b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -159,7 +159,7 @@ GEM database_cleaner (1.5.3) debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) - deckar01-task_list (1.0.5) + deckar01-task_list (1.0.6) activesupport (~> 4.0) html-pipeline rack (~> 1.0) @@ -456,7 +456,7 @@ GEM omniauth-github (1.1.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) - omniauth-gitlab (1.0.1) + omniauth-gitlab (1.0.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) omniauth-google-oauth2 (0.4.1) @@ -573,23 +573,23 @@ GEM json redcarpet (3.3.3) redis (3.2.2) - redis-actionpack (4.0.1) - actionpack (~> 4) - redis-rack (~> 1.5.0) - redis-store (~> 1.1.0) - redis-activesupport (4.1.5) - activesupport (>= 3, < 5) - redis-store (~> 1.1.0) + redis-actionpack (5.0.1) + actionpack (>= 4.0, < 6) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 1.4.0) + redis-activesupport (5.0.1) + activesupport (>= 3, < 6) + redis-store (~> 1.2.0) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redis-rack (1.5.0) + redis-rack (1.6.0) rack (~> 1.5) - redis-store (~> 1.1.0) - redis-rails (4.0.0) - redis-actionpack (~> 4) - redis-activesupport (~> 4) - redis-store (~> 1.1.0) - redis-store (1.1.7) + redis-store (~> 1.2.0) + redis-rails (5.0.1) + redis-actionpack (~> 5.0.0) + redis-activesupport (~> 5.0.0) + redis-store (~> 1.2.0) + redis-store (1.2.0) redis (>= 2.2) request_store (1.3.1) rerun (0.11.0) @@ -840,7 +840,7 @@ DEPENDENCIES creole (~> 0.5.0) d3_rails (~> 3.5.0) database_cleaner (~> 1.5.0) - deckar01-task_list (= 1.0.5) + deckar01-task_list (= 1.0.6) default_value_for (~> 3.0.0) devise (~> 4.2) devise-two-factor (~> 3.0.0) @@ -913,7 +913,7 @@ DEPENDENCIES omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) - omniauth-gitlab (~> 1.0.0) + omniauth-gitlab (~> 1.0.2) omniauth-google-oauth2 (~> 0.4.1) omniauth-kerberos (~> 0.3.0) omniauth-saml (~> 1.7.0) @@ -938,7 +938,7 @@ DEPENDENCIES redcarpet (~> 3.3.3) redis (~> 3.2) redis-namespace (~> 1.5.2) - redis-rails (~> 4.0.0) + redis-rails (~> 5.0.1) request_store (~> 1.3) rerun (~> 0.11.0) responders (~> 2.0) @@ -994,4 +994,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.5 + 1.13.6 diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 59ac9b9cef5..919107b8cb9 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -13,12 +13,12 @@ } Activities.prototype.updateTooltips = function() { - return gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + gl.utils.localTimeAgo($('.js-timeago', '.content_list')); }; Activities.prototype.reloadActivities = function() { $(".content_list").html(''); - return Pager.init(20, true); + Pager.init(20, true, false, this.updateTooltips); }; Activities.prototype.toggleFilter = function(sender) { diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7d942de0184..33c1708e1a9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,7 +13,6 @@ /*= require jquery-ui/sortable */ /*= require jquery_ujs */ /*= require jquery.endless-scroll */ -/*= require jquery.timeago */ /*= require jquery.highlight */ /*= require jquery.waitforimages */ /*= require jquery.atwho */ @@ -194,9 +193,6 @@ e.preventDefault(); return new ConfirmDangerModal(form, text); }); - $document.on('click', 'button', function () { - return $(this).blur(); - }); $('input[type="search"]').each(function () { var $this = $(this); $this.attr('value', $this.val()); @@ -238,8 +234,5 @@ // bind sidebar events new gl.Sidebar(); - - // Custom time ago - gl.utils.shortTimeAgo($('.js-short-timeago')); }); }).call(this); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index fe1a6dc7ea0..14f618fd5d5 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -2,6 +2,19 @@ $(() => { const Store = gl.issueBoards.BoardsStore; + $(document).off('created.label').on('created.label', (e, label) => { + Store.new({ + title: label.title, + position: Store.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color + } + }); + }); + $('.js-new-board-list').each(function () { const $this = $(this); new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 12e653f4122..5133e361001 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -8,56 +8,55 @@ Build.state = null; function Build(options) { - this.page_url = options.page_url; - this.build_url = options.build_url; - this.build_status = options.build_status; + options = options || $('.js-build-options').data(); + this.pageUrl = options.pageUrl; + this.buildUrl = options.buildUrl; + this.buildStatus = options.buildStatus; this.state = options.state1; - this.build_stage = options.build_stage; - this.hideSidebar = bind(this.hideSidebar, this); - this.toggleSidebar = bind(this.toggleSidebar, this); + this.buildStage = options.buildStage; this.updateDropdown = bind(this.updateDropdown, this); this.$document = $(document); clearInterval(Build.interval); // Init breakpoint checker this.bp = Breakpoints.get(); + this.initSidebar(); + this.$buildScroll = $('#js-build-scroll'); - this.populateJobs(this.build_stage); - this.updateStageDropdownText(this.build_stage); + this.populateJobs(this.buildStage); + this.updateStageDropdownText(this.buildStage); + this.sidebarOnResize(); - $(window).off('resize.build').on('resize.build', this.hideSidebar); + this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); - $('#js-build-scroll > a').off('click').on('click', this.stepTrace); + $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); + $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); this.updateArtifactRemoveDate(); if ($('#build-trace').length) { this.getInitialBuildTrace(); - this.initScrollButtons(); + this.initScrollButtonAffix(); } - if (this.build_status === "running" || this.build_status === "pending") { + if (this.buildStatus === "running" || this.buildStatus === "pending") { + // Bind autoscroll button to follow build output $('#autoscroll-button').on('click', function() { var state; state = $(this).data("state"); if ("enabled" === state) { $(this).data("state", "disabled"); - return $(this).text("enable autoscroll"); + return $(this).text("Enable autoscroll"); } else { $(this).data("state", "enabled"); - return $(this).text("disable autoscroll"); + return $(this).text("Disable autoscroll"); } - // - // Bind autoscroll button to follow build output - // }); 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 (window.location.href.split("#").first() === _this.page_url) { + if (_this.location() === _this.pageUrl) { return _this.getBuildTrace(); } }; - // - // Check for new build output if user still watching build page - // Only valid for runnig build when output changes during time - // })(this), 4000); } } @@ -72,20 +71,23 @@ top: this.sidebarTranslationLimits.max }); this.$sidebar.niceScroll(); - this.hideSidebar(); this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar); this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this)); }; + Build.prototype.location = function() { + return window.location.href.split("#")[0]; + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] return $.ajax({ - url: this.build_url, + url: this.buildUrl, dataType: 'json', - success: function(build_data) { - $('.js-build-output').html(build_data.trace_html); - if (removeRefreshStatuses.indexOf(build_data.status) >= 0) { + success: function(buildData) { + $('.js-build-output').html(buildData.trace_html); + if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { return $('.js-build-refresh').remove(); } } @@ -94,7 +96,7 @@ Build.prototype.getBuildTrace = function() { return $.ajax({ - url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)), + url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)), dataType: "json", success: (function(_this) { return function(log) { @@ -108,8 +110,8 @@ $('.js-build-output').html(log.html); } return _this.checkAutoscroll(); - } else if (log.status !== _this.build_status) { - return Turbolinks.visit(_this.page_url); + } else if (log.status !== _this.buildStatus) { + return Turbolinks.visit(_this.pageUrl); } }; })(this) @@ -122,12 +124,11 @@ } }; - Build.prototype.initScrollButtons = function() { - var $body, $buildScroll, $buildTrace; - $buildScroll = $('#js-build-scroll'); + Build.prototype.initScrollButtonAffix = function() { + var $body, $buildTrace; $body = $('body'); $buildTrace = $('#build-trace'); - return $buildScroll.affix({ + return this.$buildScroll.affix({ offset: { bottom: function() { return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top); @@ -136,18 +137,12 @@ }); }; - Build.prototype.shouldHideSidebar = function() { + Build.prototype.shouldHideSidebarForViewport = function() { var bootstrapBreakpoint; bootstrapBreakpoint = this.bp.getBreakpointSize(); return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; }; - Build.prototype.toggleSidebar = function() { - if (this.shouldHideSidebar()) { - return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed'); - } - }; - Build.prototype.translateSidebar = function(e) { var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop); if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; @@ -156,12 +151,20 @@ }); }; - Build.prototype.hideSidebar = function() { - if (this.shouldHideSidebar()) { - return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - } else { - return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - } + Build.prototype.toggleSidebar = function(shouldHide) { + var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; + this.$buildScroll.toggleClass('sidebar-expanded', shouldShow) + .toggleClass('sidebar-collapsed', shouldHide); + this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow) + .toggleClass('right-sidebar-collapsed', shouldHide); + }; + + Build.prototype.sidebarOnResize = function() { + this.toggleSidebar(this.shouldHideSidebarForViewport()); + }; + + Build.prototype.sidebarOnClick = function() { + if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); }; Build.prototype.updateArtifactRemoveDate = function() { @@ -169,7 +172,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); + return $date.text(gl.utils.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' ')); } }; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index b3f769d4129..61cc91c524b 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -80,7 +80,8 @@ success: function(html) { loading.hide(); $target.html(html); - return $('.js-timeago', $target).timeago(); + var className = '.' + $target[0].className.replace(' ', '.'); + gl.utils.localTimeAgo($('.js-timeago', className)); } }); }; diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6 index f20580b1279..744aa0afa03 100644 --- a/app/assets/javascripts/create_label.js.es6 +++ b/app/assets/javascripts/create_label.js.es6 @@ -115,6 +115,8 @@ .show(); } else { this.$dropdownBack.trigger('click'); + + $(document).trigger('created.label', label); } }); } diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 4ddafff428f..82bfdcea0ca 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -43,10 +43,6 @@ bottom: unfoldBottom, offset: offset, unfold: unfold, - // indent is used to compensate for single space indent to fit - // '+' and '-' prepended to diff lines, - // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 - indent: 1, view: file.data('view') }; return $.get(link, params, function(response) { diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 8e4fd1f19ba..756a24cc0fc 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -29,6 +29,9 @@ case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:builds:show': + new Build(); + break; case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index c74fc9ad074..afb2f0d6956 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,5 +1,7 @@ -/* eslint-disable */ -Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches; +/* global Element */ +/* eslint-disable consistent-return, max-len */ + +Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector; Element.prototype.closest = function closest(selector, selectedElement = this) { if (!selectedElement) return; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 824413bf20f..e72e2194be8 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -34,6 +34,8 @@ }, DefaultOptions: { sorter: function(query, items, searchKey) { + // Highlight first item only if at least one char was typed + this.setting.highlightFirst = query.length > 0; if ((items[0].name != null) && items[0].name === 'loading') { return items; } @@ -182,6 +184,7 @@ insertTpl: '${atwho-at}"${title}"', data: ['loading'], callbacks: { + sorter: this.DefaultOptions.sorter, beforeSave: function(milestones) { return $.map(milestones, function(m) { if (m.title == null) { @@ -236,6 +239,7 @@ displayTpl: this.Labels.template, insertTpl: '${atwho-at}${title}', callbacks: { + sorter: this.DefaultOptions.sorter, beforeSave: function(merges) { var sanitizeLabelTitle; sanitizeLabelTitle = function(title) { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 8fc498be27d..46503c290ae 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -10,6 +10,7 @@ Issuable.initSearch(); Issuable.initChecks(); Issuable.initResetFilters(); + Issuable.resetIncomingEmailToken(); return Issuable.initLabelFilterRemove(); }, initTemplates: function() { @@ -154,6 +155,27 @@ this.issuableBulkActions.willUpdateLabels = false; } return true; + }, + + resetIncomingEmailToken: function() { + $('.incoming-email-token-reset').on('click', function(e) { + e.preventDefault(); + + $.ajax({ + type: 'PUT', + url: $('.incoming-email-token-reset').attr('href'), + dataType: 'json', + success: function(response) { + $('#issue_email').val(response.new_issue_address).focus(); + }, + beforeSend: function() { + $('.incoming-email-token-reset').text('resetting...'); + }, + complete: function() { + $('.incoming-email-token-reset').text('reset it'); + } + }); + }); } }; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8447421195d..6cb3d95f984 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -119,31 +119,12 @@ parser.href = url; return parser; }; - gl.utils.cleanupBeforeFetch = function() { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); }; - - return jQuery.timefor = function(time, suffix, expiredLabel) { - var suffixFromNow, timefor; - if (!time) { - return ''; - } - suffix || (suffix = 'remaining'); - expiredLabel || (expiredLabel = 'Past due'); - jQuery.timeago.settings.allowFuture = true; - suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow; - jQuery.timeago.settings.strings.suffixFromNow = suffix; - timefor = $.timeago(time); - if (timefor.indexOf('ago') > -1) { - timefor = expiredLabel; - } - jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow; - return timefor; - }; })(window); }).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 59e526ed623..3965109dd65 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -22,51 +22,64 @@ if (setTimeago == null) { setTimeago = true; } + $timeagoEls.each(function() { - var $el; - $el = $(this); - return $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); + var $el = $(this); + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))); + + if (setTimeago) { + // Recreate with custom template + $el.tooltip({ + template: '' + }); + } + gl.utils.renderTimeago($el); }); - if (setTimeago) { - $timeagoEls.timeago(); - $timeagoEls.tooltip('destroy'); - // Recreate with custom template - return $timeagoEls.tooltip({ - template: '' - }); - } }; - w.gl.utils.shortTimeAgo = function($el) { - var shortLocale, tmpLocale; - shortLocale = { - prefixAgo: null, - prefixFromNow: null, - suffixAgo: 'ago', - suffixFromNow: 'from now', - seconds: '1 min', - minute: '1 min', - minutes: '%d mins', - hour: '1 hr', - hours: '%d hrs', - day: '1 day', - days: '%d days', - month: '1 month', - months: '%d months', - year: '1 year', - years: '%d years', - wordSeparator: ' ', - numbers: [] + w.gl.utils.getTimeago = function() { + var locale = function(number, index) { + return [ + ['less than a minute ago', 'a while'], + ['less than a minute ago', 'in %s seconds'], + ['about a minute ago', 'in 1 minute'], + ['%s minutes ago', 'in %s minutes'], + ['about an hour ago', 'in 1 hour'], + ['about %s hours ago', 'in %s hours'], + ['a day ago', 'in 1 day'], + ['%s days ago', 'in %s days'], + ['a week ago', 'in 1 week'], + ['%s weeks ago', 'in %s weeks'], + ['a month ago', 'in 1 month'], + ['%s months ago', 'in %s months'], + ['a year ago', 'in 1 year'], + ['%s years ago', 'in %s years'] + ][index]; }; - tmpLocale = $.timeago.settings.strings; - $el.each(function(el) { - var $el1; - $el1 = $(this); - return $el1.attr('title', gl.utils.formatDate($el.attr('datetime'))); - }); - $.timeago.settings.strings = shortLocale; - $el.timeago(); - $.timeago.settings.strings = tmpLocale; + + timeago.register('gl_en', locale); + return timeago(); + }; + + w.gl.utils.timeFor = function(time, suffix, expiredLabel) { + var timefor; + if (!time) { + return ''; + } + suffix || (suffix = 'remaining'); + expiredLabel || (expiredLabel = 'Past due'); + timefor = gl.utils.getTimeago().format(time).replace('in', ''); + if (timefor.indexOf('ago') > -1) { + timefor = expiredLabel; + } else { + timefor = timefor.trim() + ' ' + suffix; + } + return timefor; + }; + + w.gl.utils.renderTimeago = function($element) { + var timeagoInstance = gl.utils.getTimeago(); + timeagoInstance.render($element, 'gl_en'); }; w.gl.utils.getDayDifference = function(a, b) { @@ -75,7 +88,7 @@ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); return Math.floor((date2 - date1) / millisecondsPerDay); - } + }; })(window); diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js new file mode 100644 index 00000000000..42606dd2d46 --- /dev/null +++ b/app/assets/javascripts/lib/utils/timeago.js @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2016 hustcc + * License: MIT + * Version: v2.0.2 + * https://github.com/hustcc/timeago.js + * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js) +**/ +/* eslint-disable */ +/* jshint expr: true */ +!function (root, factory) { + if (typeof module === 'object' && module.exports) + module.exports = factory(root); + else + root.timeago = factory(root); +}(typeof window !== 'undefined' ? window : this, +function () { + var cnt = 0, // the timer counter, for timer key + indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'), + + // build-in locales: en & zh_CN + locales = { + 'en': function(number, index) { + if (index === 0) return ['just now', 'right now']; + var unit = indexMapEn[parseInt(index / 2)]; + if (number > 1) unit += 's'; + return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit]; + }, + }, + // second, minute, hour, day, week, month, year(365 days) + SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12], + SEC_ARRAY_LEN = 6, + ATTR_DATETIME = 'datetime'; + + // format Date / string / timestamp to Date instance. + function toDate(input) { + if (input instanceof Date) return input; + if (!isNaN(input)) return new Date(toInt(input)); + if (/^\d+$/.test(input)) return new Date(toInt(input, 10)); + input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds + .replace(/-/, '/').replace(/-/, '/') + .replace(/T/, ' ').replace(/Z/, ' UTC') + .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400 + return new Date(input); + } + // change f into int, remove Decimal. just for code compression + function toInt(f) { + return parseInt(f); + } + // format the diff second to *** time ago, with setting locale + function formatDiff(diff, locale, defaultLocale) { + // if locale is not exist, use defaultLocale. + // if defaultLocale is not exist, use build-in `en`. + // be sure of no error when locale is not exist. + locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en'); + // if (! locales[locale]) locale = defaultLocale; + var i = 0; + agoin = diff < 0 ? 1 : 0; // timein or timeago + diff = Math.abs(diff); + + for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + diff /= SEC_ARRAY[i]; + } + diff = toInt(diff); + i *= 2; + + if (diff > (i === 0 ? 9 : 1)) i += 1; + return locales[locale](diff, i)[agoin].replace('%s', diff); + } + // calculate the diff second between date to be formated an now date. + function diffSec(date, nowDate) { + nowDate = nowDate ? toDate(nowDate) : new Date(); + return (nowDate - toDate(date)) / 1000; + } + /** + * nextInterval: calculate the next interval time. + * - diff: the diff sec between now and date to be formated. + * + * What's the meaning? + * diff = 61 then return 59 + * diff = 3601 (an hour + 1 second), then return 3599 + * make the interval with high performace. + **/ + function nextInterval(diff) { + var rst = 1, i = 0, d = Math.abs(diff); + for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + diff /= SEC_ARRAY[i]; + rst *= SEC_ARRAY[i]; + } + // return leftSec(d, rst); + d = d % rst; + d = d ? rst - d : rst; + return Math.ceil(d); + } + // get the datetime attribute, jQuery and DOM + function getDateAttr(node) { + if (node.getAttribute) return node.getAttribute(ATTR_DATETIME); + if(node.attr) return node.attr(ATTR_DATETIME); + } + /** + * timeago: the function to get `timeago` instance. + * - nowDate: the relative date, default is new Date(). + * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * var timeago = timeagoLib(); // all use default. + * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. + * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. + * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. + **/ + function Timeago(nowDate, defaultLocale) { + var timers = {}; // real-time render timers + // if do not set the defaultLocale, set it with `en` + if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale + // what the timer will do + function doRender(node, date, locale, cnt) { + var diff = diffSec(date, nowDate); + node.innerHTML = formatDiff(diff, locale, defaultLocale); + // waiting %s seconds, do the next render + timers['k' + cnt] = setTimeout(function() { + doRender(node, date, locale, cnt); + }, nextInterval(diff) * 1000); + } + /** + * nextInterval: calculate the next interval time. + * - diff: the diff sec between now and date to be formated. + * + * What's the meaning? + * diff = 61 then return 59 + * diff = 3601 (an hour + 1 second), then return 3599 + * make the interval with high performace. + **/ + // this.nextInterval = function(diff) { // for dev test + // var rst = 1, i = 0, d = Math.abs(diff); + // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) { + // diff /= SEC_ARRAY[i]; + // rst *= SEC_ARRAY[i]; + // } + // // return leftSec(d, rst); + // d = d % rst; + // d = d ? rst - d : rst; + // return Math.ceil(d); + // }; // for dev test + /** + * format: format the date to *** time ago, with setting or default locale + * - date: the date / string / timestamp to be formated + * - locale: the formated string's locale name, e.g. en / zh_CN + * + * How to use it? + * var timeago = require('timeago.js')(); + * timeago.format(new Date(), 'pl'); // Date instance + * timeago.format('2016-09-10', 'fr'); // formated date string + * timeago.format(1473473400269); // timestamp with ms + **/ + this.format = function(date, locale) { + return formatDiff(diffSec(date, nowDate), locale, defaultLocale); + }; + /** + * render: render the DOM real-time. + * - nodes: which nodes will be rendered. + * - locale: the locale name used to format date. + * + * How to use it? + * var timeago = new require('timeago.js')(); + * // 1. javascript selector + * timeago.render(document.querySelectorAll('.need_to_be_rendered')); + * // 2. use jQuery selector + * timeago.render($('.need_to_be_rendered'), 'pl'); + * + * Notice: please be sure the dom has attribute `datetime`. + **/ + this.render = function(nodes, locale) { + if (nodes.length === undefined) nodes = [nodes]; + for (var i = 0; i < nodes.length; i++) { + doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item + } + }; + /** + * cancel: cancel all the timers which are doing real-time render. + * + * How to use it? + * var timeago = new require('timeago.js')(); + * timeago.render(document.querySelectorAll('.need_to_be_rendered')); + * timeago.cancel(); // will stop all the timer, stop render in real time. + **/ + this.cancel = function() { + for (var key in timers) { + clearTimeout(timers[key]); + } + timers = {}; + }; + /** + * setLocale: set the default locale name. + * + * How to use it? + * var timeago = require('timeago.js'); + * timeago = new timeago(); + * timeago.setLocale('fr'); + **/ + this.setLocale = function(locale) { + defaultLocale = locale; + }; + return this; + } + /** + * timeago: the function to get `timeago` instance. + * - nowDate: the relative date, default is new Date(). + * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you. + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * var timeago = timeagoLib(); // all use default. + * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago. + * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`. + * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前. + **/ + function timeagoFactory(nowDate, defaultLocale) { + return new Timeago(nowDate, defaultLocale); + } + /** + * register: register a new language locale + * - locale: locale name, e.g. en / zh_CN, notice the standard. + * - localeFunc: the locale process function + * + * How to use it? + * var timeagoLib = require('timeago.js'); + * + * timeagoLib.register('the locale name', the_locale_func); + * // or + * timeagoLib.register('pl', require('timeago.js/locales/pl')); + **/ + timeagoFactory.register = function(locale, localeFunc) { + locales[locale] = localeFunc; + }; + + return timeagoFactory; +}); \ No newline at end of file diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 3a2fe454b68..56c87af3226 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -218,7 +218,7 @@ } if (environment.deployed_at && environment.deployed_at_formatted) { - environment.deployed_at = $.timeago(environment.deployed_at) + '.'; + environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.'; } else { $('.js-environment-timeago', $template).remove(); environment.name += '.'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index c909b53dc21..d1cd38ad110 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -162,7 +162,7 @@ if (data.milestone != null) { data.milestone.namespace = _this.currentProject.namespace; data.milestone.path = _this.currentProject.path; - data.milestone.remaining = $.timefor(data.milestone.due_date); + data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); } else { diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 42d6799c82f..a192273a180 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -9,6 +9,8 @@ (function() { $(function() { + if (!$(".network-graph").length) return; + var network_graph; network_graph = new Network({ url: $(".network-graph").attr('data-url'), diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index ce117c3fba5..202ed5ae8fe 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -4,7 +4,7 @@ margin-right: $margin-right; } -.avatar-container { +.avatar-circle { float: left; margin-right: 15px; border-radius: $avatar_radius; @@ -27,7 +27,7 @@ } .avatar { - @extend .avatar-container; + @extend .avatar-circle; width: 40px; height: 40px; padding: 0; @@ -64,8 +64,8 @@ &.s160 { font-size: 96px; line-height: 158px; } } -.image-container { - @extend .avatar-container; +.avatar-container { + @extend .avatar-circle; overflow: hidden; display: flex; @@ -76,4 +76,4 @@ margin: 0; align-self: center; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ed21ad83a1c..e7aff2d0cec 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -6,7 +6,6 @@ &:focus, &:active { - outline: none; background-color: $btn-active-gray; box-shadow: $gl-btn-active-background; } @@ -267,10 +266,6 @@ outline: none; } - &:focus { - outline: none; - } - &:active { outline: none; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 3e34ec98427..583c17e4a83 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -38,7 +38,6 @@ text-align: left; border: 1px solid $border-color; border-radius: $border-radius-base; - outline: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -55,6 +54,10 @@ } } + &.no-outline { + outline: 0; + } + &:hover, { border-color: $dropdown-toggle-hover-border-color; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4993ca7572a..5a34132112a 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -100,10 +100,6 @@ header { &:hover { background-color: $btn-gray-hover; } - - &:focus { - outline: none; - } } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fcaf5e18633..ce864c2de5e 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,7 +58,6 @@ &:active, &:focus { text-decoration: none; - outline: none; } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 13749f1b7bd..920ce249b9a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -63,7 +63,7 @@ } .select2-highlighted { - background: #3084bb !important; + background: $gl-link-color !important; } .select2-results li.select2-result-with-children > .select2-result-label { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d74c14ee2a4..44c445c0543 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -83,7 +83,6 @@ display: block; text-decoration: none; font-weight: normal; - outline: none; &:hover, &:active, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index be2a7ceefff..e0d00759c9c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -103,7 +103,7 @@ $gl-text-color-light: #8c8c8c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; -$gl-link-color: #3084bb; +$gl-link-color: #3777b0; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; @@ -197,7 +197,7 @@ $line-number-new: #ddfbe6; $line-number-select: #fbf2da; $match-line: $gray-light; $table-border-gray: #f0f0f0; -$line-target-blue: #eaf3fc; +$line-target-blue: #f6faff; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 6300ac9662f..f1d311cabbe 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -14,18 +14,10 @@ } } - .autoscroll-container { - position: fixed; - bottom: 20px; - right: 20px; - z-index: 100; - } - .scroll-controls { - &.affix-top { - position: absolute; - top: 10px; - right: 25px; + .scroll-step { + width: 31px; + margin: 0 0 0 auto; } &.affix-bottom { @@ -34,13 +26,13 @@ } &.affix { - right: 30px; + right: 25px; bottom: 15px; z-index: 1; + } - @media (min-width: $screen-md-min) { - right: 26%; - } + &.sidebar-expanded { + right: #{$gutter_width + ($gl-padding * 2)}; } a { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 8ecf7fcb96d..47d3e72679b 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -36,9 +36,42 @@ padding: 10px 0; margin-bottom: 0; - .commit-options-dropdown-caret { - @media (max-width: $screen-sm) { - margin-left: 0; + @media (min-width: $screen-sm-min) { + display: flex; + align-items: center; + + .commit-meta { + flex: 1; + } + } + + .commit-hash-full { + @media (max-width: $screen-sm-max) { + width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } + } + + .commit-action-buttons { + i { + color: $gl-icon-color; + font-size: 13px; + margin-right: 3px; + } + + @media (max-width: $screen-xs-max) { + .dropdown { + width: 100%; + margin-top: 10px; + } + + .dropdown-toggle { + width: 100%; + } } } } @@ -188,17 +221,6 @@ } } -.commit-action-buttons { - position: relative; - top: -1px; - - i { - color: $gl-icon-color; - font-size: 13px; - margin-right: 3px; - } -} - /* * Commit message textarea for web editor and * custom merge request message diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index fde138c874d..99fdea15218 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -92,20 +92,6 @@ &.noteable_line { position: relative; - - &.old { - &::before { - content: '-'; - position: absolute; - } - } - - &.new { - &::before { - content: '+'; - position: absolute; - } - } } span { @@ -151,8 +137,9 @@ .line_content { display: block; margin: 0; - padding: 0 0.5em; + padding: 0 1.5em; border: none; + position: relative; &.parallel { display: table-cell; @@ -161,6 +148,22 @@ word-break: break-all; } } + + &.old { + &::before { + content: '-'; + position: absolute; + left: 0.5em; + } + } + + &.new { + &::before { + content: '+'; + position: absolute; + left: 0.5em; + } + } } .text-file.diff-wrap-lines table .line_holder td span { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 032feae8854..19ab198c2e7 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -228,7 +228,6 @@ $colors: ( position: absolute; right: 10px; padding: 0; - outline: none; color: #fff; width: 75px; // static width to make 2 buttons have same width height: 19px; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ede29db1979..6fab97a71aa 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -23,6 +23,10 @@ color: $md-link-color; } +.private-tokens-reset div.reset-action:not(:first-child) { + padding-top: 15px; +} + .oauth-buttons { .btn-group { margin-right: 10px; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bf688af50e2..b4761df3f23 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -31,7 +31,6 @@ padding-right: 20px; border: none; font-size: 14px; - outline: none; padding: 0; margin-left: 5px; line-height: 25px; @@ -229,6 +228,5 @@ &:hover, &:focus { color: $gl-link-color; - outline: none; } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 86e808314f4..52e0256943a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -117,6 +117,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :send_user_confirmation_email, :container_registry_token_expire_delay, :enabled_git_access_protocol, + :housekeeping_enabled, + :housekeeping_bitmaps_enabled, + :housekeeping_incremental_repack_period, + :housekeeping_full_repack_period, + :housekeeping_gc_period, repository_storages: [], restricted_visibility_levels: [], import_sources: [], diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 37600ed875c..517ad4f03f3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -192,9 +192,10 @@ class ApplicationController < ActionController::Base end # JSON for infinite scroll via Pager object - def pager_json(partial, count) + def pager_json(partial, count, locals = {}) html = render_to_string( partial, + locals: locals, layout: false, formats: [:html] ) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 7e4da73bc11..c736200a104 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -12,7 +12,7 @@ class JwtController < ApplicationController return head :not_found unless service result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities || []) + execute(authentication_abilities: @authentication_result.authentication_abilities) render json: result, status: result[:http_status] end @@ -20,7 +20,7 @@ class JwtController < ApplicationController private def authenticate_project_or_user - @authentication_result = Gitlab::Auth::Result.new + @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities) authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f71e0a1302b..f0c71725ea8 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -26,7 +26,15 @@ class ProfilesController < Profiles::ApplicationController def reset_private_token if current_user.reset_authentication_token! - flash[:notice] = "Token was successfully updated" + flash[:notice] = "Private token was successfully reset" + end + + redirect_to profile_account_path + end + + def reset_incoming_email_token + if current_user.reset_incoming_email_token! + flash[:notice] = "Incoming email token was successfully reset" end redirect_to profile_account_path diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index c2e7bf1ffec..aba87b6144b 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -26,8 +26,15 @@ class Projects::CommitsController < Projects::ApplicationController respond_to do |format| format.html - format.json { pager_json("projects/commits/_commits", @commits.size) } format.atom { render layout: false } + + format.json do + pager_json( + 'projects/commits/_commits', + @commits.size, + project: @project, + ref: @ref) + end end end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 383e184d796..3f41916e6d3 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -21,10 +21,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController def authenticate_user @authentication_result = Gitlab::Auth::Result.new - if project && project.public? && download_request? - return # Allow access - end - if allow_basic_auth? && basic_auth_provided? login, password = user_name_and_password(request) @@ -41,6 +37,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController send_final_spnego_response return # Allow access end + elsif project && download_request? && Guest.can?(:download_code, project) + @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) + + return # Allow access end send_challenges diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 662d38b10a5..13caeb42d40 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -78,11 +78,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack - if user - access_check.allowed? - else - ci? || project.public? - end + access_check.allowed? || ci? end def access diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 30f1cf4e5be..9f104d903cc 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -352,13 +352,23 @@ class Projects::MergeRequestsController < Projects::ApplicationController def branch_from # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project - @commit = @repository.commit(params[:ref]) if params[:ref].present? + + if params[:ref].present? + @ref = params[:ref] + @commit = @repository.commit(@ref) + end + render layout: false end def branch_to @target_project = selected_target_project - @commit = @target_project.commit(params[:ref]) if params[:ref].present? + + if params[:ref].present? + @ref = params[:ref] + @commit = @target_project.commit(@ref) + end + render layout: false end @@ -589,12 +599,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def merge_request_params - params.require(:merge_request).permit( - :title, :assignee_id, :source_project_id, :source_branch, - :target_project_id, :target_branch, :milestone_id, - :state_event, :description, :task_num, :force_remove_source_branch, - :lock_version, label_ids: [] - ) + params.require(:merge_request) + .permit(merge_request_params_ce) + end + + def merge_request_params_ce + [ + :assignee_id, + :description, + :force_remove_source_branch, + :lock_version, + :milestone_id, + :source_branch, + :source_project_id, + :state_event, + :target_branch, + :target_project_id, + :task_num, + :title, + + label_ids: [] + ] end def merge_params diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 34318391dd9..33a152ad34f 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -5,17 +5,29 @@ class Projects::NetworkController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! + before_action :assign_commit def show @url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json)) @commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s") respond_to do |format| - format.html + format.html do + if @options[:extended_sha1] && !@commit + flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist." + end + end format.json do @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref]) end end end + + def assign_commit + return if params[:extended_sha1].blank? + + @options[:extended_sha1] = params[:extended_sha1] + @commit = @repo.commit(@options[:extended_sha1]) + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index bce5e29d8d8..a8a18b4fa16 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -2,9 +2,9 @@ class ProjectsController < Projects::ApplicationController include IssuableCollections include ExtractsPath - before_action :authenticate_user!, except: [:show, :activity, :refs] - before_action :project, except: [:new, :create] - before_action :repository, except: [:new, :create] + before_action :authenticate_user!, except: [:index, :show, :activity, :refs] + before_action :project, except: [:index, :new, :create] + before_action :repository, except: [:index, :new, :create] before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] @@ -160,6 +160,13 @@ class ProjectsController < Projects::ApplicationController end end + def new_issue_address + return render_404 unless Gitlab::IncomingEmail.supports_issue_creation? + + current_user.reset_incoming_email_token! + render json: { new_issue_address: @project.new_issue_address(current_user) } + end + def archive return access_denied! unless can?(current_user, :archive_project, @project) @@ -318,25 +325,44 @@ class ProjectsController < Projects::ApplicationController end def project_params - project_feature_attributes = - { - project_feature_attributes: - [ - :issues_access_level, :builds_access_level, - :wiki_access_level, :merge_requests_access_level, - :snippets_access_level, :repository_access_level - ] - } + params.require(:project) + .permit(project_params_ce) + end - params.require(:project).permit( - :name, :path, :description, :issues_tracker, :tag_list, :runners_token, + def project_params_ce + [ + :avatar, + :build_allow_git_fetch, + :build_coverage_regex, + :build_timeout_in_minutes, :container_registry_enabled, - :issues_tracker_id, :default_branch, - :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, - :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, - :lfs_enabled, project_feature_attributes - ) + :default_branch, + :description, + :import_url, + :issues_tracker, + :issues_tracker_id, + :last_activity_at, + :lfs_enabled, + :name, + :namespace_id, + :only_allow_merge_if_all_discussions_are_resolved, + :only_allow_merge_if_build_succeeds, + :path, + :public_builds, + :request_access_enabled, + :runners_token, + :tag_list, + :visibility_level, + + project_feature_attributes: %i[ + builds_access_level + issues_access_level + merge_requests_access_level + repository_access_level + snippets_access_level + wiki_access_level + ] + ] end def repo_exists? diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index d01e0dedf52..b666aa01d6b 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -16,7 +16,7 @@ class SearchController < ApplicationController @group = nil unless can?(current_user, :read_group, @group) end - return if params[:search].nil? || params[:search].blank? + return if params[:search].blank? @search_term = params[:search] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6a881b271d7..c4508ccc3b9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -104,8 +104,7 @@ class UsersController < ApplicationController end def contributions_calendar - @contributions_calendar ||= Gitlab::ContributionsCalendar. - new(contributed_projects, user) + @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user) end def load_events diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index cc2073081b5..6297b2db369 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -61,31 +61,26 @@ class IssuableFinder def project return @project if defined?(@project) - if project? - @project = Project.find(params[:project_id]) + project = Project.find(params[:project_id]) + project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project) - unless Ability.allowed?(current_user, :read_project, @project) - @project = nil - end - else - @project = nil - end - - @project + @project = project end def projects return @projects if defined?(@projects) + return @projects = project if project? - if project? - @projects = project - elsif current_user && params[:authorized_only].presence && !current_user_related? - @projects = current_user.authorized_projects.reorder(nil) - elsif group - @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil) - else - @projects = ProjectsFinder.new.execute(current_user).reorder(nil) - end + projects = + if current_user && params[:authorized_only].presence && !current_user_related? + current_user.authorized_projects + elsif group + GroupProjectsFinder.new(group).execute(current_user) + else + ProjectsFinder.new.execute(current_user) + end + + @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) end def search diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb new file mode 100644 index 00000000000..5d27d30eaa3 --- /dev/null +++ b/app/helpers/accounts_helper.rb @@ -0,0 +1,5 @@ +module AccountsHelper + def incoming_email_token_enabled? + current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation? + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ebd78bf9888..c816b616631 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -151,7 +151,6 @@ module ApplicationHelper # time - Time object # placement - Tooltip placement String (default: "top") # html_class - Custom class for `time` element (default: "time_ago") - # skip_js - When true, exclude the `script` tag (default: false) # # By default also includes a `script` element with Javascript necessary to # initialize the `timeago` jQuery extension. If this method is called many @@ -163,22 +162,19 @@ module ApplicationHelper # `html_class` argument is provided. # # Returns an HTML-safe String - def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false) + def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false) css_classes = short_format ? 'js-short-timeago' : 'js-timeago' css_classes << " #{html_class}" unless html_class.blank? - css_classes << ' js-timeago-pending' unless skip_js element = content_tag :time, time.to_s, class: css_classes, - datetime: time.to_time.getutc.iso8601, title: time.to_time.in_time_zone.to_s(:medium), - data: { toggle: 'tooltip', placement: placement, container: 'body' } - - unless skip_js - element << javascript_tag( - "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" - ) - end + datetime: time.to_time.getutc.iso8601, + data: { + toggle: 'tooltip', + placement: placement, + container: 'body' + } element end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index e13b7cdd707..07ff6fb9488 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -179,33 +179,6 @@ module BlobHelper } end - def selected_template(issuable) - templates = issuable_templates(issuable) - params[:issuable_template] if templates.include?(params[:issuable_template]) - end - - def can_add_template?(issuable) - names = issuable_templates(issuable) - names.empty? && can?(current_user, :push_code, @project) && !@project.private? - end - - def merge_request_template_names - @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) - end - - def issue_template_names - @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) - end - - def issuable_templates(issuable) - @issuable_templates ||= - if issuable.is_a?(Issue) - issue_template_names - elsif issuable.is_a?(MergeRequest) - merge_request_template_names - end - end - def ref_project @ref_project ||= @target_project || @project end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index f3aaff9140d..fde297c588e 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -5,4 +5,14 @@ module BuildsHelper build_class += ' retried' if build.retried? build_class end + + def javascript_build_options + { + page_url: namespace_project_build_url(@project.namespace, @project, @build), + build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), + build_status: @build.status, + build_stage: @build.stage, + state1: @build.trace_with_state[:state] + } + end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index fabe5c1f63a..895c3d728ad 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -56,10 +56,18 @@ module CiStatusHelper custom_icon(icon_name) end - def render_commit_status(commit, tooltip_placement: 'auto left') + def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left') project = commit.project - path = pipelines_namespace_project_commit_path(project.namespace, project, commit) - render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement) + path = pipelines_namespace_project_commit_path( + project.namespace, + project, + commit) + + render_status_with_link( + 'commit', + commit.status(ref), + path, + tooltip_placement: tooltip_placement) end def render_pipeline_status(pipeline, tooltip_placement: 'auto left') diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 33dcee49aee..ed402b698fb 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -25,9 +25,11 @@ module CommitsHelper end end - def commit_to_html(commit, project, inline = true) - template = inline ? "inline_commit" : "commit" - render "projects/commits/#{template}", commit: commit, project: project unless commit.nil? + def commit_to_html(commit, ref, project) + render 'projects/commits/commit', + commit: commit, + ref: ref, + project: project end # Breadcrumb links for a Project and, if applicable, a tree path diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb new file mode 100644 index 00000000000..8893209b314 --- /dev/null +++ b/app/helpers/components_helper.rb @@ -0,0 +1,9 @@ +module ComponentsHelper + def gitlab_workhorse_version + if request.headers['Gitlab-Workhorse'].present? + request.headers['Gitlab-Workhorse'].split('-').first + else + Gitlab::Workhorse.version + end + end +end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 0725c3f4c56..f489f9aa0d6 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -51,12 +51,11 @@ module DiffHelper html.html_safe end - def diff_line_content(line, line_type = nil) + def diff_line_content(line) if line.blank? - "  ".html_safe + " ".html_safe else - line[0] = ' ' if %w[new old].include?(line_type) - line + line.sub(/^[\-+ ]/, '').html_safe end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ef6cfb235a9..8127c3f3ee3 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,6 +30,33 @@ module IssuablesHelper end end + def can_add_template?(issuable) + names = issuable_templates(issuable) + names.empty? && can?(current_user, :push_code, @project) && !@project.private? + end + + def template_dropdown_tag(issuable, &block) + title = selected_template(issuable) || "Choose a template" + options = { + toggle_class: 'js-issuable-selector', + title: title, + filter: true, + placeholder: 'Filter', + footer_content: true, + data: { + data: issuable_templates(issuable), + field_name: 'issuable_template', + selected: selected_template(issuable), + project_path: ref_project.path, + namespace_path: ref_project.namespace.path + } + } + + dropdown_tag(title, options: options) do + capture(&block) + end + end + def user_dropdown_label(user_id, default_label) return default_label if user_id.nil? return "Unassigned" if user_id == "0" @@ -153,4 +180,28 @@ module IssuablesHelper hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-')) end + + def issuable_templates(issuable) + @issuable_templates ||= + case issuable + when Issue + issue_template_names + when MergeRequest + merge_request_template_names + else + raise 'Unknown issuable type!' + end + end + + def merge_request_template_names + @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) + end + + def issue_template_names + @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) + end + + def selected_template(issuable) + params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template]) + end end diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index 95b60aeab5f..d3966ba1f10 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -1,6 +1,6 @@ module LfsHelper include Gitlab::Routing.url_helpers - + def require_lfs_enabled! return if Gitlab.config.lfs.enabled @@ -27,7 +27,7 @@ module LfsHelper def lfs_download_access? return false unless project.lfs_enabled? - project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? + ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? end def user_can_download_code? diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 7e8369d0a05..03cc8f2b6bd 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -74,4 +74,13 @@ module NotificationsHelper return unless notification_setting.source_type hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id end + + def notification_event_name(event) + case event + when :success_pipeline + 'Successful pipeline' + else + event.to_s.humanize + end + end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index a9db8bb2b82..09c69786791 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -61,6 +61,10 @@ module TodosHelper } end + def todos_filter_empty? + todos_filter_params.values.none? + end + def todos_filter_path(options = {}) without = options.delete(:without) diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 61a574d3dc0..79c3c2e62c5 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,6 +1,6 @@ class BaseMailer < ActionMailer::Base - add_template_helper ApplicationHelper - add_template_helper GitlabMarkdownHelper + helper ApplicationHelper + helper GitlabMarkdownHelper attr_accessor :current_user helper_method :current_user, :can? diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 601c8b5cd62..9460a6cd2be 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,22 +1,27 @@ module Emails module Pipelines - def pipeline_success_email(pipeline, to) - pipeline_mail(pipeline, to, 'succeeded') + def pipeline_success_email(pipeline, recipients) + pipeline_mail(pipeline, recipients, 'succeeded') end - def pipeline_failed_email(pipeline, to) - pipeline_mail(pipeline, to, 'failed') + def pipeline_failed_email(pipeline, recipients) + pipeline_mail(pipeline, recipients, 'failed') end private - def pipeline_mail(pipeline, to, status) + def pipeline_mail(pipeline, recipients, status) @project = pipeline.project @pipeline = pipeline @merge_request = pipeline.merge_requests.first add_headers - mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format| + # We use bcc here because we don't want to generate this emails for a + # thousand times. This could be potentially expensive in a loop, and + # recipients would contain all project watchers so it could be a lot. + mail(bcc: recipients, + subject: pipeline_subject(status), + skip_premailer: true) do |format| format.html { render layout: false } format.text end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index eca6ec29767..0bc1c19e9cd 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -10,12 +10,12 @@ class Notify < BaseMailer include Emails::Pipelines include Emails::Members - add_template_helper MergeRequestsHelper - add_template_helper DiffHelper - add_template_helper BlobHelper - add_template_helper EmailsHelper - add_template_helper MembersHelper - add_template_helper GitlabRoutingHelper + helper MergeRequestsHelper + helper DiffHelper + helper BlobHelper + helper EmailsHelper + helper MembersHelper + helper GitlabRoutingHelper def test_email(recipient_email, subject, body) mail(to: recipient_email, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6e7a90e7d9c..bb60cc8736c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -85,6 +85,18 @@ class ApplicationSetting < ActiveRecord::Base presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' }, if: :domain_blacklist_enabled? + validates :housekeeping_incremental_repack_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :housekeeping_full_repack_period, + presence: true, + numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period } + + validates :housekeeping_gc_period, + presence: true, + numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -168,6 +180,11 @@ class ApplicationSetting < ActiveRecord::Base container_registry_token_expire_delay: 5, repository_storages: ['default'], user_default_external: false, + housekeeping_enabled: true, + housekeeping_bitmaps_enabled: true, + housekeeping_incremental_repack_period: 10, + housekeeping_full_repack_period: 50, + housekeeping_gc_period: 200, ) end @@ -202,11 +219,7 @@ class ApplicationSetting < ActiveRecord::Base end def repository_storages - value = read_attribute(:repository_storages) - value = [value] if value.is_a?(String) - value = [] if value.nil? - - value + Array(read_attribute(:repository_storages)) end # repository_storage is still required in the API. Remove in 9.0 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d3432632899..3fee6c18770 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -81,6 +81,12 @@ module Ci PipelineHooksWorker.perform_async(id) end end + + after_transition any => [:success, :failed] do |pipeline| + pipeline.run_after_commit do + PipelineNotificationWorker.perform_async(pipeline.id) + end + end end # ref can't be HEAD or SHA, can only be branch/tag name @@ -109,6 +115,11 @@ module Ci project.id end + # For now the only user who participates is the user who triggered + def participants(_current_user = nil) + Array(user) + end + def valid_commit_sha if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") diff --git a/app/models/commit.rb b/app/models/commit.rb index e64fd1e0c1b..9e7fde9503d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -226,12 +226,19 @@ class Commit end def pipelines - @pipeline ||= project.pipelines.where(sha: sha) + project.pipelines.where(sha: sha) end - def status - return @status if defined?(@status) - @status ||= pipelines.status + def status(ref = nil) + @statuses ||= {} + + if @statuses.key?(ref) + @statuses[ref] + elsif ref + @statuses[ref] = pipelines.where(ref: ref).status + else + @statuses[ref] = pipelines.status + end end def revert_branch_name diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 613444e0d70..664bb594aa9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -183,6 +183,10 @@ module Issuable grouping_columns end + + def to_ability_name + model_name.singular + end end def today? @@ -244,7 +248,7 @@ module Issuable # issuable.class # => MergeRequest # issuable.to_ability_name # => "merge_request" def to_ability_name - self.class.to_s.underscore + self.class.to_ability_name end # Returns a Hash of attributes to be used for Twitter card metadata @@ -286,6 +290,11 @@ module Issuable false end + def assignee_or_author?(user) + # We're comparing IDs here so we don't need to load any associations. + author_id == user.id || assignee_id == user.id + end + def record_metrics metrics = self.metrics || create_metrics metrics.record! diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 24c7b26d223..04d30f46210 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -4,17 +4,21 @@ module TokenAuthenticatable private def write_new_token(token_field) - new_token = generate_token(token_field) + new_token = generate_available_token(token_field) write_attribute(token_field, new_token) end - def generate_token(token_field) + def generate_available_token(token_field) loop do - token = Devise.friendly_token + token = generate_token(token_field) break token unless self.class.unscoped.find_by(token_field => token) end end + def generate_token(token_field) + Devise.friendly_token + end + class_methods do def authentication_token_fields @token_fields || [] diff --git a/app/models/event.rb b/app/models/event.rb index 43e67069b70..c76d88b1c7b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -49,6 +49,7 @@ class Event < ActiveRecord::Base update_all(updated_at: Time.now) end + # Update Gitlab::ContributionsCalendar#activity_dates if this changes def contributions where("action = ? OR (target_type in (?) AND action in (?))", Event::PUSHED, ["MergeRequest", "Issue"], @@ -62,7 +63,7 @@ class Event < ActiveRecord::Base def visible_to_user?(user = nil) if push? - true + Ability.allowed?(user, :download_code, project) elsif membership_changed? true elsif created_project? diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index fd9a8c1b8b7..91b508eb325 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -29,6 +29,15 @@ class ExternalIssue @project end + def project_id + @project.id + end + + # Pattern used to extract `JIRA-123` issue references from text + def self.reference_pattern + @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} + end + def to_reference(_from_project = nil) id end diff --git a/app/models/guest.rb b/app/models/guest.rb new file mode 100644 index 00000000000..01285ca1264 --- /dev/null +++ b/app/models/guest.rb @@ -0,0 +1,7 @@ +class Guest + class << self + def can?(action, subject) + Ability.allowed?(nil, action, subject) + end + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 4f02b02c488..adbca510ef7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -250,31 +250,11 @@ class Issue < ActiveRecord::Base # Returns `true` if the current issue can be viewed by either a logged in User # or an anonymous user. def visible_to_user?(user = nil) + return false unless project.feature_available?(:issues, user) + user ? readable_by?(user) : publicly_visible? end - # Returns `true` if the given User can read the current Issue. - def readable_by?(user) - if user.admin? - true - elsif project.owner == user - true - elsif confidential? - author == user || - assignee == user || - project.team.member?(user, Gitlab::Access::REPORTER) - else - project.public? || - project.internal? && !user.external? || - project.team.member?(user) - end - end - - # Returns `true` if this Issue is visible to everybody. - def publicly_visible? - project.public? && !confidential? - end - def overdue? due_date.try(:past?) || false end @@ -297,4 +277,32 @@ class Issue < ActiveRecord::Base end end end + + private + + # Returns `true` if the given User can read the current Issue. + # + # This method duplicates the same check of issue_policy.rb + # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8 + # Make sure to sync this method with issue_policy.rb + def readable_by?(user) + if user.admin? + true + elsif project.owner == user + true + elsif confidential? + author == user || + assignee == user || + project.team.member?(user, Gitlab::Access::REPORTER) + else + project.public? || + project.internal? && !user.external? || + project.team.member?(user) + end + end + + # Returns `true` if this Issue is visible to everybody. + def publicly_visible? + project.public? && !confidential? + end end diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb new file mode 100644 index 00000000000..f0b7d9914c8 --- /dev/null +++ b/app/models/issue_collection.rb @@ -0,0 +1,42 @@ +# IssueCollection can be used to reduce a list of issues down to a subset. +# +# IssueCollection is not meant to be some sort of Enumerable, instead it's meant +# to take a list of issues and return a new list of issues based on some +# criteria. For example, given a list of issues you may want to return a list of +# issues that can be read or updated by a given user. +class IssueCollection + attr_reader :collection + + def initialize(collection) + @collection = collection + end + + # Returns all the issues that can be updated by the user. + def updatable_by_user(user) + return collection if user.admin? + + # Given all the issue projects we get a list of projects that the current + # user has at least reporter access to. + projects_with_reporter_access = user. + projects_with_reporter_access_limited_to(project_ids). + pluck(:id) + + collection.select do |issue| + if projects_with_reporter_access.include?(issue.project_id) + true + elsif issue.is_a?(Issue) + issue.assignee_or_author?(user) + else + false + end + end + end + + alias_method :visible_to, :updatable_by_user + + private + + def project_ids + @project_ids ||= collection.map(&:project_id).uniq + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6b8ac3fb48b..d76feb9680e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -425,6 +425,7 @@ class MergeRequest < ActiveRecord::Base return false if work_in_progress? return false if broken? return false unless skip_ci_check || mergeable_ci_state? + return false unless mergeable_discussions_state? true end @@ -493,6 +494,12 @@ class MergeRequest < ActiveRecord::Base discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?) end + def mergeable_discussions_state? + return true unless project.only_allow_merge_if_all_discussions_are_resolved? + + discussions_resolved? + end + def hook_attrs attrs = { source: source_project.try(:hook_attrs), diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 121b598b8f3..43fc218de2b 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base :reopen_merge_request, :close_merge_request, :reassign_merge_request, - :merge_merge_request + :merge_merge_request, + :failed_pipeline, + :success_pipeline ] store :events, accessors: EMAIL_EVENTS, coder: JSON diff --git a/app/models/project.rb b/app/models/project.rb index cf931f64c03..bbe590b5a8a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -207,8 +207,38 @@ class Project < ActiveRecord::Base scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } - scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') } - scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') } + scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } + + # "enabled" here means "not disabled". It includes private features! + scope :with_feature_enabled, ->(feature) { + access_level_attribute = ProjectFeature.access_level_attribute(feature) + with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] }) + } + + # Picks a feature where the level is exactly that given. + scope :with_feature_access_level, ->(feature, level) { + access_level_attribute = ProjectFeature.access_level_attribute(feature) + with_project_feature.where(project_features: { access_level_attribute => level }) + } + + scope :with_builds_enabled, -> { with_feature_enabled(:builds) } + scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + + # project features may be "disabled", "internal" or "enabled". If "internal", + # they are only available to team members. This scope returns projects where + # the feature is either enabled, or internal with permission for the user. + def self.with_feature_available_for_user(feature, user) + return with_feature_enabled(feature) if user.try(:admin?) + + unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) + return unconditional if user.nil? + + conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) + authorized = user.authorized_projects.merge(conditional.reorder(nil)) + + union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) + where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) + end scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } @@ -624,13 +654,12 @@ class Project < ActiveRecord::Base end def new_issue_address(author) - # This feature is disabled for the time being. - return nil + return unless Gitlab::IncomingEmail.supports_issue_creation? && author - if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode - Gitlab::IncomingEmail.reply_address( - "#{path_with_namespace}+#{author.authentication_token}") - end + author.ensure_incoming_email_token! + + Gitlab::IncomingEmail.reply_address( + "#{path_with_namespace}+#{author.incoming_email_token}") end def build_commit_note(commit) @@ -1067,10 +1096,6 @@ class Project < ActiveRecord::Base forks.count end - def find_label(name) - labels.find_by(name: name) - end - def origin_merge_requests merge_requests.where(source_project_id: self.id) end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index b37ce1d3cf6..34fd5a57b5e 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base FEATURES = %i(issues merge_requests wiki snippets builds repository) + class << self + def access_level_attribute(feature) + feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) + raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature) + + "#{feature}_access_level".to_sym + end + end + # Default scopes force us to unscope here since a service may need to check # permissions for a project in pending_delete # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to @@ -35,9 +44,8 @@ class ProjectFeature < ActiveRecord::Base default_value_for :repository_access_level, value: ENABLED, allows_nil: false def feature_available?(feature, user) - raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) - - get_permission(user, public_send("#{feature}_access_level")) + access_level = public_send(ProjectFeature.access_level_attribute(feature)) + get_permission(user, access_level) end def builds_enabled? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 0a493b7a12b..2dbe0075465 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -163,6 +163,21 @@ class JiraService < IssueTrackerService add_comment(data, issue_key) end + # reason why service cannot be tested + def disabled_title + "Please fill in Password and Username." + end + + def can_test? + username.present? && password.present? + end + + # JIRA does not need test data. + # We are requesting the project that belongs to the project key. + def test_data(user = nil, project = nil) + nil + end + def test_settings return unless url.present? # Test settings by getting the project diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index ec3c1bc85ee..745f9bd1b43 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,10 +1,7 @@ class PipelinesEmailService < Service prop_accessor :recipients - boolean_accessor :add_pusher boolean_accessor :notify_only_broken_pipelines - validates :recipients, - presence: true, - if: ->(s) { s.activated? && !s.add_pusher? } + validates :recipients, presence: true, if: :activated? def initialize_properties self.properties ||= { notify_only_broken_pipelines: true } @@ -34,8 +31,8 @@ class PipelinesEmailService < Service return unless all_recipients.any? - pipeline = Ci::Pipeline.find(data[:object_attributes][:id]) - Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients) + pipeline_id = data[:object_attributes][:id] + PipelineNotificationWorker.new.perform(pipeline_id, all_recipients) end def can_test? @@ -57,9 +54,6 @@ class PipelinesEmailService < Service { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' }, - { type: 'checkbox', - name: 'add_pusher', - label: 'Add pusher to recipients list' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] @@ -85,12 +79,6 @@ class PipelinesEmailService < Service end def retrieve_recipients(data) - all_recipients = recipients.to_s.split(',').reject(&:blank?) - - if add_pusher? && data[:user].try(:[], :email) - all_recipients << data[:user][:email] - end - - all_recipients + recipients.to_s.split(',').reject(&:blank?) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 30be7262438..7d06ce1e85b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1064,6 +1064,10 @@ class Repository end def search_files(query, ref) + unless exists? && has_visible_content? && query.present? + return [] + end + offset = 2 args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) diff --git a/app/models/user.rb b/app/models/user.rb index 65e96ee6b2e..3813df6684e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,7 @@ class User < ActiveRecord::Base DEFAULT_NOTIFICATION_LEVEL = :participating add_authentication_token_field :authentication_token + add_authentication_token_field :incoming_email_token default_value_for :admin, false default_value_for(:external) { current_application_settings.user_default_external } @@ -119,7 +120,7 @@ class User < ActiveRecord::Base before_validation :set_public_email, if: ->(user) { user.public_email_changed? } after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } - before_save :ensure_authentication_token + before_save :ensure_authentication_token, :ensure_incoming_email_token before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit @@ -444,6 +445,16 @@ class User < ActiveRecord::Base Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})") end + # Returns the projects this user has reporter (or greater) access to, limited + # to at most the given projects. + # + # This method is useful when you have a list of projects and want to + # efficiently check to which of these projects the user has at least reporter + # access. + def projects_with_reporter_access_limited_to(projects) + authorized_projects(Gitlab::Access::REPORTER).where(id: projects) + end + def viewable_starred_projects starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", [Project::PUBLIC, Project::INTERNAL]) @@ -946,4 +957,13 @@ class User < ActiveRecord::Base signup_domain =~ regexp end end + + def generate_token(token_field) + if token_field == :incoming_email_token + # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address. + SecureRandom.hex.to_i(16).to_s(36) + else + super + end + end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 2232e231cf8..8b25332b73c 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -5,7 +5,7 @@ module Ci # If we can't read build we should also not have that # ability when looking at this in context of commit_status - %w(read create update admin).each do |rule| + %w[read create update admin].each do |rule| cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build" end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb new file mode 100644 index 00000000000..3d2eef1c50c --- /dev/null +++ b/app/policies/ci/pipeline_policy.rb @@ -0,0 +1,4 @@ +module Ci + class PipelinePolicy < BuildPolicy + end +end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index c253f9a9399..9501e499507 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -4,7 +4,7 @@ class IssuablePolicy < BasePolicy end def rules - if @user && (@subject.author == @user || @subject.assignee == @user) + if @user && @subject.assignee_or_author?(@user) can! :"read_#{action_name}" can! :"update_#{action_name}" end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index bd1811a3c54..88f3179c6ff 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -1,4 +1,8 @@ class IssuePolicy < IssuablePolicy + # This class duplicates the same check of Issue#readable_by? for performance reasons + # Make sure to sync this class checks with issue.rb to avoid security problems. + # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. + def issue @subject end @@ -8,9 +12,8 @@ class IssuePolicy < IssuablePolicy if @subject.confidential? && !can_read_confidential? cannot! :read_issue - cannot! :admin_issue cannot! :update_issue - cannot! :read_issue + cannot! :admin_issue end end @@ -18,11 +21,7 @@ class IssuePolicy < IssuablePolicy def can_read_confidential? return false unless @user - return true if @user.admin? - return true if @subject.author == @user - return true if @subject.assignee == @user - return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER) - false + IssueCollection.new([@subject]).visible_to(@user).any? end end diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb new file mode 100644 index 00000000000..de9a181db90 --- /dev/null +++ b/app/serializers/base_serializer.rb @@ -0,0 +1,18 @@ +class BaseSerializer + def initialize(parameters = {}) + @request = EntityRequest.new(parameters) + end + + def represent(resource, opts = {}) + self.class.entity_class + .represent(resource, opts.merge(request: @request)) + end + + def self.entity(entity_class) + @entity_class ||= entity_class + end + + def self.entity_class + @entity_class + end +end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb new file mode 100644 index 00000000000..3d9ac66de0e --- /dev/null +++ b/app/serializers/build_entity.rb @@ -0,0 +1,24 @@ +class BuildEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :name + + expose :build_url do |build| + url_to(:namespace_project_build, build) + end + + expose :retry_url do |build| + url_to(:retry_namespace_project_build, build) + end + + expose :play_url, if: ->(build, _) { build.manual? } do |build| + url_to(:play_namespace_project_build, build) + end + + private + + def url_to(route, build) + send("#{route}_url", build.project.namespace, build.project, build) + end +end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb new file mode 100644 index 00000000000..f7eba6fc1e3 --- /dev/null +++ b/app/serializers/commit_entity.rb @@ -0,0 +1,12 @@ +class CommitEntity < API::Entities::RepoCommit + include RequestAwareEntity + + expose :author, using: UserEntity + + expose :commit_url do |commit| + namespace_project_tree_url( + request.project.namespace, + request.project, + id: commit.id) + end +end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb new file mode 100644 index 00000000000..ad6fc8d665b --- /dev/null +++ b/app/serializers/deployment_entity.rb @@ -0,0 +1,27 @@ +class DeploymentEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :iid + expose :sha + + expose :ref do + expose :name do |deployment| + deployment.ref + end + + expose :ref_url do |deployment| + namespace_project_tree_url( + deployment.project.namespace, + deployment.project, + id: deployment.ref) + end + end + + expose :tag + expose :last? + expose :user, using: UserEntity + expose :commit, using: CommitEntity + expose :deployable, using: BuildEntity + expose :manual_actions, using: BuildEntity +end diff --git a/app/serializers/entity_request.rb b/app/serializers/entity_request.rb new file mode 100644 index 00000000000..456ba1174c0 --- /dev/null +++ b/app/serializers/entity_request.rb @@ -0,0 +1,12 @@ +class EntityRequest + # We use EntityRequest object to collect parameters and variables + # from the controller. Because options that are being passed to the entity + # do appear in each entity object in the chain, we need a way to pass data + # that is present in the controller (see #20045). + # + def initialize(parameters) + parameters.each do |key, value| + define_singleton_method(key) { value } + end + end +end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb new file mode 100644 index 00000000000..ee4392cc46d --- /dev/null +++ b/app/serializers/environment_entity.rb @@ -0,0 +1,20 @@ +class EnvironmentEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :name + expose :state + expose :external_url + expose :environment_type + expose :last_deployment, using: DeploymentEntity + expose :stoppable? + + expose :environment_url do |environment| + namespace_project_environment_url( + environment.project.namespace, + environment.project, + environment) + end + + expose :created_at, :updated_at +end diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb new file mode 100644 index 00000000000..91955542f25 --- /dev/null +++ b/app/serializers/environment_serializer.rb @@ -0,0 +1,3 @@ +class EnvironmentSerializer < BaseSerializer + entity EnvironmentEntity +end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb new file mode 100644 index 00000000000..ff8c1142abc --- /dev/null +++ b/app/serializers/request_aware_entity.rb @@ -0,0 +1,11 @@ +module RequestAwareEntity + extend ActiveSupport::Concern + + included do + include Gitlab::Routing.url_helpers + end + + def request + @options.fetch(:request) + end +end diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb new file mode 100644 index 00000000000..43754ea94f7 --- /dev/null +++ b/app/serializers/user_entity.rb @@ -0,0 +1,2 @@ +class UserEntity < API::Entities::UserBasic +end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 8ea88da8a53..c00c5aebf57 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -9,8 +9,8 @@ module Auth return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled - unless current_user || project - return error('DENIED', status: 403, message: 'access forbidden') unless scope + unless scope || current_user || project + return error('DENIED', status: 403, message: 'access forbidden') end { token: authorized_token(scope).encoded } @@ -76,7 +76,7 @@ module Auth case requested_action when 'pull' - requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project) + build_can_pull?(requested_project) || user_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) else @@ -92,23 +92,23 @@ module Auth # Build can: # 1. pull from its own project (for ex. a build) # 2. read images from dependent projects if creator of build is a team member - @authentication_abilities.include?(:build_read_container_image) && + has_authentication_ability?(:build_read_container_image) && (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) end def user_can_pull?(requested_project) - @authentication_abilities.include?(:read_container_image) && + has_authentication_ability?(:read_container_image) && can?(current_user, :read_container_image, requested_project) end def build_can_push?(requested_project) # Build can push only to the project from which it originates - @authentication_abilities.include?(:build_create_container_image) && + has_authentication_ability?(:build_create_container_image) && requested_project == project end def user_can_push?(requested_project) - @authentication_abilities.include?(:create_container_image) && + has_authentication_ability?(:create_container_image) && can?(current_user, :create_container_image, requested_project) end @@ -118,5 +118,9 @@ module Auth http_status: status } end + + def has_authentication_ability?(capability) + (@authentication_abilities || []).include?(capability) + end end end diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb deleted file mode 100644 index ceb182801f7..00000000000 --- a/app/services/ci/send_pipeline_notification_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Ci - class SendPipelineNotificationService - attr_reader :pipeline - - def initialize(new_pipeline) - @pipeline = new_pipeline - end - - def execute(recipients) - email_template = "pipeline_#{pipeline.status}_email" - - return unless Notify.respond_to?(email_template) - - recipients.each do |to| - Notify.public_send(email_template, pipeline, to).deliver_later - end - end - end -end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e8415862de5..de313095bed 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -105,35 +105,11 @@ class GitPushService < BaseService # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. def process_commit_messages - is_default_branch = is_default_branch? - - authors = Hash.new do |hash, commit| - email = commit.author_email - next hash[email] if hash.has_key?(email) - - hash[email] = commit_user(commit) - end + default = is_default_branch? @push_commits.each do |commit| - # Keep track of the issues that will be actually closed because they are on a default branch. - # Hence, when creating cross-reference notes, the not-closed issues (on non-default branches) - # will also have cross-reference. - closed_issues = [] - - if is_default_branch - # Close issues if these commits were pushed to the project's default branch and the commit message matches the - # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to - # a different branch. - closed_issues = commit.closes_issues(current_user) - closed_issues.each do |issue| - if can?(current_user, :update_issue, issue) - Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit) - end - end - end - - commit.create_cross_references!(authors[commit], closed_issues) - update_issue_metrics(commit, authors) + ProcessCommitWorker. + perform_async(project.id, current_user.id, commit.id, default) end end @@ -176,11 +152,4 @@ class GitPushService < BaseService def branch_name @branch_name ||= Gitlab::Git.ref_name(params[:ref]) end - - def update_issue_metrics(commit, authors) - mentioned_issues = commit.all_references(authors[commit]).issues - - Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). - update_all(first_mentioned_in_commit_at: commit.committed_date) - end end diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index 45cca216ccc..ab4c51386a4 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -1,8 +1,21 @@ module Issues class CloseService < Issues::BaseService + # Closes the supplied issue if the current user is able to do so. def execute(issue, commit: nil, notifications: true, system_note: true) return issue unless can?(current_user, :update_issue, issue) + close_issue(issue, + commit: commit, + notifications: notifications, + system_note: system_note) + end + + # Closes the supplied issue without checking if the user is authorized to + # do so. + # + # The code calling this method is responsible for ensuring that a user is + # allowed to close the given issue. + def close_issue(issue, commit: nil, notifications: true, system_note: true) if project.jira_tracker? && project.jira_service.active project.jira_service.execute(commit, issue) todo_service.close_issue(issue, current_user) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 72712afc07e..6697840cc26 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -312,6 +312,22 @@ class NotificationService mailer.project_was_not_exported_email(current_user, project, errors).deliver_later end + def pipeline_finished(pipeline, recipients = nil) + email_template = "pipeline_#{pipeline.status}_email" + + return unless mailer.respond_to?(email_template) + + recipients ||= build_recipients( + pipeline, + pipeline.project, + nil, # The acting user, who won't be added to recipients + action: pipeline.status).map(&:notification_email) + + if recipients.any? + mailer.public_send(email_template, pipeline, recipients).deliver_later + end + end + protected # Get project/group users with CUSTOM notification level @@ -475,9 +491,14 @@ class NotificationService end def reject_users_without_access(recipients, target) - return recipients unless target.is_a?(Issuable) + ability = case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end - ability = :"read_#{target.to_ability_name}" + return recipients unless ability recipients.select do |user| user.can?(ability, target) @@ -624,6 +645,6 @@ class NotificationService # Build event key to search on custom notification level # Check NotificationSetting::EMAIL_EVENTS def build_custom_key(action, object) - "#{action}_#{object.class.name.underscore}".to_sym + "#{action}_#{object.class.model_name.name.underscore}".to_sym end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index c3dfc8cfbe8..4b8946f8ee2 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -7,6 +7,8 @@ # module Projects class HousekeepingService < BaseService + include Gitlab::CurrentSettings + LEASE_TIMEOUT = 3600 class LeaseTaken < StandardError @@ -20,13 +22,14 @@ module Projects end def execute - raise LeaseTaken unless try_obtain_lease + lease_uuid = try_obtain_lease + raise LeaseTaken unless lease_uuid.present? - execute_gitlab_shell_gc + execute_gitlab_shell_gc(lease_uuid) end def needed? - @project.pushes_since_gc >= 10 + pushes_since_gc > 0 && period_match? && housekeeping_enabled? end def increment! @@ -37,19 +40,59 @@ module Projects private - def execute_gitlab_shell_gc - GitGarbageCollectWorker.perform_async(@project.id) + def execute_gitlab_shell_gc(lease_uuid) + GitGarbageCollectWorker.perform_async(@project.id, task, lease_key, lease_uuid) ensure - Gitlab::Metrics.measure(:reset_pushes_since_gc) do - @project.reset_pushes_since_gc + if pushes_since_gc >= gc_period + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @project.reset_pushes_since_gc + end end end def try_obtain_lease Gitlab::Metrics.measure(:obtain_housekeeping_lease) do - lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT) + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) lease.try_obtain end end + + def lease_key + "project_housekeeping:#{@project.id}" + end + + def pushes_since_gc + @project.pushes_since_gc + end + + def task + if pushes_since_gc % gc_period == 0 + :gc + elsif pushes_since_gc % full_repack_period == 0 + :full_repack + else + :incremental_repack + end + end + + def period_match? + [gc_period, full_repack_period, repack_period].any? { |period| pushes_since_gc % period == 0 } + end + + def housekeeping_enabled? + current_application_settings.housekeeping_enabled + end + + def gc_period + current_application_settings.housekeeping_gc_period + end + + def full_repack_period + current_application_settings.housekeeping_full_repack_period + end + + def repack_period + current_application_settings.housekeeping_incremental_repack_period + end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 28003e5f509..450ec322f2c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -422,5 +422,44 @@ Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. + %fieldset + %legend Automatic Git repository housekeeping + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :housekeeping_enabled do + = f.check_box :housekeeping_enabled + Enable automatic repository housekeeping (git repack, git gc) + .help-block + If you keep automatic housekeeping disabled for a long time Git + repository access on your GitLab server will become slower and your + repositories will use more disk space. We recommend to always leave + this enabled. + .checkbox + = f.label :housekeeping_bitmaps_enabled do + = f.check_box :housekeeping_bitmaps_enabled + Enable Git pack file bitmap creation + .help-block + Creating pack file bitmaps makes housekeeping take a little longer but + bitmaps should accelerate 'git clone' performance. + .form-group + = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .help-block + Number of Git pushes after which an incremental 'git repack' is run. + .form-group + = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_full_repack_period, class: 'form-control' + .help-block + Number of Git pushes after which a full 'git repack' is run. + .form-group + = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_gc_period, class: 'form-control' + .help-block + Number of Git pushes after which 'git gc' is run. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 90798c47d97..1db2150f336 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -87,7 +87,7 @@ %p GitLab Workhorse %span.pull-right - = Gitlab::Workhorse.version + = gitlab_workhorse_version %p GitLab API %span.pull-right diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 05c88ca1cc8..664bb417c6a 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -16,7 +16,7 @@ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) - .image-container.s40 + .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to [:admin, group], class: 'group-name' do diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index a7c1a4f5038..40871e32913 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -13,7 +13,7 @@ Group info: %ul.well-list %li - .image-container.s60 + .avatar-container.s60 = image_tag group_icon(@group), class: "avatar s60" %li %span.light Name: diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 10dce6f3d8f..b37b8d4fee7 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -76,7 +76,7 @@ .title = link_to [:admin, project.namespace.becomes(Namespace), project] do .dash-project-avatar - .image-container.s40 + .avatar-container.s40 = project_icon(project, alt: '', class: 'avatar project-avatar s40') %span.project-full-name %span.namespace-name diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index e247eebc3fc..5b2465e25ee 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -82,15 +82,19 @@ - elsif current_user.todos.any? .todos-all-done = render "shared/empty_states/todos_all_done.svg" - %h4.text-center - Good job! Looks like you don't have any todos left. - %p.text-center - Are you looking for things to do? Take a look at - = succeed "," do - = link_to "the opened issues", issues_dashboard_path - contribute to - = link_to "merge requests", merge_requests_dashboard_path - or mention someone in a comment to assign a new todo automatically. + - if todos_filter_empty? + %h4.text-center + Good job! Looks like you don't have any todos left. + %p.text-center + Are you looking for things to do? Take a look at + = succeed "," do + = link_to "the opened issues", issues_dashboard_path + contribute to + = link_to "merge requests", merge_requests_dashboard_path + or mention someone in a comment to assign a new todo automatically. + - else + %h4.text-center + There are no todos to show. - else .todos-empty .todos-empty-hero diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 31fdcc5e21b..5c318cd3b8b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,7 +1,7 @@ - if event.visible_to_user?(current_user) .event-item{ class: event_row_class(event) } .event-item-timestamp - #{time_ago_with_tooltip(event.created_at, skip_js: true)} + #{time_ago_with_tooltip(event.created_at)} = cache [event, current_application_settings, "v2.2"] do = author_avatar(event, size: 40) diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 2f90c19d4b4..2706e8692d1 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -8,7 +8,7 @@ .form-group .col-sm-offset-2.col-sm-10 - .image-container.s160 + .avatar-container.s160 = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' %p.light - if @group.avatar? diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 275581b3af8..b439b40a75a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -6,7 +6,7 @@ .cover-block.groups-cover-block %div{ class: container_class } - .image-container.s70.group-avatar + .avatar-container.s70.group-avatar = image_tag group_icon(@group), class: "avatar s70 avatar-tile" .group-info .cover-title diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index d7386105b7d..8e65bd12c56 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,7 @@ .location-badge= label .search-input-wrap .dropdown{ data: { url: search_autocomplete_path } } - = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } + = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url } .dropdown-menu.dropdown-select = dropdown_content do %ul diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 0995826775a..38c852f0a3a 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -103,11 +103,11 @@ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} = @merge_request.to_reference .commit{style: "color:#5c5c5c;font-weight:300;"} = @pipeline.git_commit_message.truncate(50) @@ -134,7 +134,7 @@ %tr.pre-section %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"} Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = "\##{@pipeline.id}" had = failed.size @@ -158,7 +158,7 @@ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"} = build.stage %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"} - %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"} + %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"} = build.name %tr.build-log %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"} @@ -168,10 +168,10 @@ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ %div - %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications · - %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index cf9c1d4d72c..697c8d19257 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -103,11 +103,11 @@ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"} %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"} - %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = @pipeline.short_sha - if @merge_request in - %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"} + %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"} = @merge_request.to_reference .commit{style: "color:#5c5c5c;font-weight:300;"} = @pipeline.git_commit_message.truncate(50) @@ -135,7 +135,7 @@ - build_count = @pipeline.statuses.latest.size - stage_count = @pipeline.stages.size Pipeline - %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"} + %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"} = "\##{@pipeline.id}" successfully completed = "#{build_count} #{'build'.pluralize(build_count)}" @@ -145,10 +145,10 @@ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"} %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/ %div - %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications + %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications · - %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help + %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help %div You're receiving this email because of your account on = succeed "." do - %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host + %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index e2e974ba072..72f658d1b68 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -8,24 +8,36 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Private Token + = incoming_email_token_enabled? ? "Private Tokens" : "Private Token" %p - Your private token is used to access application resources without authentication. - .col-lg-9 - = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f| + Keep + = incoming_email_token_enabled? ? "these tokens" : "this token" + secret, anyone with access to them can interact with GitLab as if they were you. + .col-lg-9.private-tokens-reset + .reset-action %p.cgray - if current_user.private_token - = label_tag "token", "Private token", class: "label-light" - = text_field_tag "token", current_user.private_token, class: "form-control" + = label_tag "private-token", "Private token", class: "label-light" + = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()" - else - %span You don`t have one yet. Click generate to fix it. - %p.help-block - It can be used for atom feeds or the API. Keep it secret! + %span You don't have one yet. Click generate to fix it. + %p.help-block + Your private token is used to access the API and Atom feeds without username/password authentication. .prepend-top-default - if current_user.private_token - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" + = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token" - else = f.submit 'Generate', class: "btn btn-default" + - if incoming_email_token_enabled? + .reset-action + %p.cgray + = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light' + = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()" + %p.help-block + Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses. + .prepend-top-default + = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token" + %hr .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index e67b66d1fff..5a04c3318cf 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,7 @@ - empty_repo = @project.empty_repo? .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } %div{ class: container_class } - .image-container.s70.project-avatar + .avatar-container.s70.project-avatar = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title = @project.name diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 630ae7d6140..7f530708947 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,10 +1,12 @@ -- if commit.status - = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do - = ci_icon_for_status(commit.status) - = ci_label_for_status(commit.status) +- ref = local_assigns.fetch(:ref) +- status = commit.status(ref) +- if status + = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + = ci_label_for_status(status) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" · -#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by +#{time_ago_with_tooltip(commit.committed_date)} by = commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 80053dd501b..6e143c4b570 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -12,3 +12,7 @@ %span.descr Builds need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + .checkbox + = f.label :only_allow_merge_if_all_discussions_are_resolved do + = f.check_box :only_allow_merge_if_all_discussions_are_resolved + %strong Only allow merge requests to be merged if all discussions are resolved diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index dfb96305f48..cadfe5a3e30 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -32,7 +32,7 @@ .light = commit_author_link(commit, avatar: false) authored - #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} + #{time_ago_with_tooltip(commit.committed_date)} %td.line-numbers - line_count = blame_group[:lines].count - (current_line...(current_line + line_count)).each do |i| diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 3ffc3fcb7ac..149ee7c59d6 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -20,7 +20,7 @@ %ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - = render blob_commit, project: @project + = render blob_commit, project: @project, ref: @ref %div#blob-content-holder.blob-content-holder %article.file-holder diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index b5e8b0bf6eb..ae7a7ecb392 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Builds" -- trace_with_state = @build.trace_with_state - header_title project_title(@project, "Builds", project_builds_path(@project)) = render "projects/pipelines/head", build_subnav: true @@ -28,32 +27,27 @@ Runners page .prepend-top-default - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - if @build.erased? .erased.alert.alert-warning - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - else #js-build-scroll.scroll-controls - = link_to '#build-trace', class: 'btn' do - %i.fa.fa-angle-up - = link_to '#down-build-trace', class: 'btn' do - %i.fa.fa-angle-down + .scroll-step + = link_to '#build-trace', class: 'btn' do + %i.fa.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa.fa-angle-down + - if @build.active? + .autoscroll-container + %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} + Enable autoscroll %pre.build-trace#build-trace %code.bash.js-build-output = icon("refresh spin", class: "js-build-refresh") - #down-build-trace + #down-build-trace = render "sidebar" - :javascript - new Build({ - page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}", - build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", - build_status: "#{@build.status}", - build_stage: "#{@build.stage}", - state1: "#{trace_with_state[:state]}" - }) +.js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 1f748d73d06..2a2d24be736 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -59,7 +59,7 @@ - if pipeline.finished_at %p.finished-at = icon("calendar") - #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)} + #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)} %td.pipeline-actions.hidden-xs .controls.pull-right diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index d8c95376b94..0ebc38d16cf 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,25 +1,25 @@ .commit-info-row.commit-info-row-header - %span.hidden-xs.hidden-sm Commit - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace js-details-short" - = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do - %span.text-expander - \... - %span.js-details-content.hide - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm" - = clipboard_button(clipboard_text: @commit.id) - %span.hidden-xs authored - #{time_ago_with_tooltip(@commit.authored_date)} - %span by - = author_avatar(@commit, size: 24) - %strong - = commit_author_link(@commit, avatar: true, size: 24) - - if @commit.different_committer? - %span.light Committed by + .commit-meta + %strong Commit + %strong.monospace.js-details-short= @commit.short_id + = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do + %span.text-expander + \... + %span.js-details-content.hide + %strong.monospace.commit-hash-full= @commit.id + = clipboard_button(clipboard_text: @commit.id) + %span.hidden-xs authored + #{time_ago_with_tooltip(@commit.authored_date)} + %span by + = author_avatar(@commit, size: 24) %strong - = commit_committer_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.committed_date)} - - .pull-right.commit-action-buttons + = commit_author_link(@commit, avatar: true, size: 24) + - if @commit.different_committer? + %span.light Committed by + %strong + = commit_committer_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.committed_date)} + .commit-action-buttons - if defined?(@notes_count) && @notes_count > 0 %span.btn.disabled.btn-grouped.hidden-xs.append-right-10 = icon('comment') @@ -28,8 +28,8 @@ Browse Files .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } - %span.hidden-xs Options - = icon('caret-down', class: ".commit-options-dropdown-caret") + %span Options + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right %li.visible-xs-block.visible-sm-block = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index fb48aef0559..9f80a974d64 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,3 +1,4 @@ +- ref = local_assigns.fetch(:ref) - if @note_counts - note_count = @note_counts.fetch(commit.id, 0) - else @@ -18,15 +19,15 @@ %span.commit-row-message.visible-xs-inline · = commit.short_id - - if commit.status + - if commit.status(ref) .visible-xs-inline - = render_commit_status(commit) + = render_commit_status(commit, ref: ref) - if commit.description? %a.text-expander.hidden-xs.js-toggle-button ... .commit-actions.hidden-xs - - if commit.status - = render_commit_status(commit) + - if commit.status(ref) + = render_commit_status(commit, ref: ref) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 46e4de40042..ce416caa494 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list= render commits, project: @project + %ul.content-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index dd12eae8f7e..48756c68941 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,13 +1,11 @@ -- unless defined?(project) - - project = @project - +- ref = local_assigns.fetch(:ref) - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" %li.commits-row %ul.list-unstyled.commit-list - = render commits, project: project + = render commits, project: project, ref: ref - if hidden > 0 %li.alert.alert-warning diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 876c8002627..9628cbd1634 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -35,7 +35,7 @@ %div{id: dom_id(@project)} %ol#commits-list.list-unstyled.content_list - = render "commits", project: @project + = render 'commits', project: @project, ref: @ref = spinner :javascript diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 73993f35b39..d3ed8e1bf38 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -1,4 +1,4 @@ -%i.fa.diff-toggle-caret +%i.fa.diff-toggle-caret.fa-fw - if defined?(blob) && blob && diff_file.submodule? %span = icon('archive fw') diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 7042e9f1fc9..a3e4b5b777e 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -25,9 +25,9 @@ %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) }< - if email - %pre= diff_line_content(line.text, type) + %pre= diff_line_content(line.text) - else - = diff_line_content(line.text, type) + = diff_line_content(line.text) - discussions = local_assigns.fetch(:discussions, nil) - if discussions && !line.meta? diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a5422966617..0aa8801c2d8 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -118,7 +118,7 @@ Project avatar .form-group - if @project.avatar? - .image-container.s160 + .avatar-container.s160 = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml index 72669372497..d2038a2be68 100644 --- a/app/views/projects/issues/_issue_by_email.html.haml +++ b/app/views/projects/issues/_issue_by_email.html.haml @@ -12,16 +12,23 @@ Create new issue by email .modal-body %p - Write an email to the below email address. (This is a private email address, so keep it secret.) + You can create a new issue inside this project by sending an email to the following email address: .email-modal-input-group.input-group = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true .input-group-btn = clipboard_button(clipboard_target: '#issue_email') %p - Send an email to this address to create an issue. + The subject will be used as the title of the new issue, and the message will be the description. + + = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 + and styling with + = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 + are supported. + %p - Use the subject line as the title of your issue. - %p - Use the message as the body of your issue (feel free to include some nice - = succeed ")." do - = link_to "Markdown", help_page_path('markdown', 'markdown') + This is a private email address, generated just for you. + + Anyone who gets ahold of it can create issues as if they were you. + You should + = link_to 'reset it', new_issue_address_namespace_project_path(@project.namespace, @project), class: 'incoming-email-token-reset' + if that ever happens. diff --git a/app/views/projects/merge_requests/branch_from.html.haml b/app/views/projects/merge_requests/branch_from.html.haml index 4f90dde6fa8..3837c4b388d 100644 --- a/app/views/projects/merge_requests/branch_from.html.haml +++ b/app/views/projects/merge_requests/branch_from.html.haml @@ -1 +1,2 @@ -= commit_to_html(@commit, @source_project, false) +- if @commit + = commit_to_html(@commit, @ref, @source_project) diff --git a/app/views/projects/merge_requests/branch_to.html.haml b/app/views/projects/merge_requests/branch_to.html.haml index 67a7a6bcec9..d69b71790a0 100644 --- a/app/views/projects/merge_requests/branch_to.html.haml +++ b/app/views/projects/merge_requests/branch_to.html.haml @@ -1 +1,2 @@ -= commit_to_html(@commit, @target_project, false) +- if @commit + = commit_to_html(@commit, @ref, @target_project) diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 61020516bcf..a0e12fb3f38 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -3,4 +3,4 @@ Most recent commits displayed first %ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.source_project + = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 842b6df310d..01314eb37d0 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -23,8 +23,10 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed? + - elsif !@merge_request.mergeable_ci_state? = render 'projects/merge_requests/widget/open/build_failed' + - elsif !@merge_request.mergeable_discussions_state? + = render 'projects/merge_requests/widget/open/unresolved_discussions' - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' diff --git a/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml new file mode 100644 index 00000000000..35d5677ee37 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_unresolved_discussions.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon('exclamation-triangle') + This merge request has unresolved discussions + +%p + Please resolve these discussions to allow this merge request to be merged. \ No newline at end of file diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 29df1bab04e..d8951e69242 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -17,5 +17,6 @@ = check_box_tag :filter_ref, 1, @options[:filter_ref] %span Begin with the selected commit - .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } } - = spinner nil, true + - if @commit + .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } } + = spinner nil, true diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 1141168f037..44fa4b60343 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -16,3 +16,6 @@ var url = "#{escape_javascript(@more_log_url)}"; ajaxGet(url); } + +:plain + gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody')); \ No newline at end of file diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 752fbc21a11..b41edeb2c7e 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -12,6 +12,9 @@ = form.submit 'Save changes', class: 'btn btn-save'   - if @service.valid? && @service.activated? - - disabled = @service.can_test? ? '':'disabled' - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled}", title: @service.disabled_title + - unless @service.can_test? + - disabled_class = 'disabled' + - disabled_title = @service.disabled_title + + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d2570598501..4de95036eef 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -79,7 +79,7 @@ = render 'shared/notifications/button', notification_setting: @notification_setting - if @repository.commit .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, project: @project + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml index 5b2d83d6b92..f34eaf89027 100644 --- a/app/views/search/results/_commit.html.haml +++ b/app/views/search/results/_commit.html.haml @@ -1 +1 @@ -= render 'projects/commits/commit', project: @project, commit: commit += render 'projects/commits/commit', project: @project, commit: commit, ref: nil diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 562291a61df..19221e3391f 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -24,7 +24,7 @@ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) - .image-container.s40 + .avatar-container.s40 = image_tag group_icon(group), class: "avatar s40 hidden-xs" .title = link_to group, class: 'group-name' do diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0ace6be8f4e..3176af9c19b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,4 +1,5 @@ - project = @target_project || @project + = form_errors(issuable) - if @conflict @@ -11,23 +12,9 @@ .form-group = f.label :title, class: 'control-label' - - issuable_template_names = issuable_templates(issuable) + = render 'shared/issuable/form/template_selector', issuable: issuable - - if issuable_template_names.any? - .col-sm-3.col-lg-2 - .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } } - - title = selected_template(issuable) || "Choose a template" - - = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector', - title: title, filter: true, placeholder: 'Filter', footer_content: true, - data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: ref_project.path, namespace_path: ref_project.namespace.path } } ) do - %ul.dropdown-footer-list - %li - %a.no-template - No template - %a.reset-template - Reset template - %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } + %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', class: 'form-control pad', required: true @@ -142,7 +129,7 @@ .col-sm-10.col-sm-offset-2 .checkbox = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0' + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_template_selector.html.haml b/app/views/shared/issuable/form/_template_selector.html.haml new file mode 100644 index 00000000000..d613bd31d81 --- /dev/null +++ b/app/views/shared/issuable/form/_template_selector.html.haml @@ -0,0 +1,13 @@ +- issuable = local_assigns.fetch(:issuable, nil) + +- return unless issuable && issuable_templates(issuable).any? + +.col-sm-3.col-lg-2 + .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } } + = template_dropdown_tag(issuable) do + %ul.dropdown-footer-list + %li + %a.no-template + No template + %a.reset-template + Reset template diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index b704981e3db..a82fc95df84 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -27,5 +27,5 @@ %label{ for: field_id } = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) %strong - = event.to_s.humanize + = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 3d2122a159c..264391fe84f 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -32,7 +32,7 @@ = link_to project_path(project), class: dom_class(project) do - if avatar .dash-project-avatar - .image-container.s40 + .avatar-container.s40 - if use_creator_avatar = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - else diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index 78f253f9023..eff6c80d144 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,5 +1,5 @@ .clearfix - groups.each do |group| = link_to group, class: 'profile-groups-avatars inline', title: group.name do - .image-container.s40 + .avatar-container.s40 = image_tag group_icon(group), class: 'avatar group-avatar s40' diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index 65f8093b5b0..d369b639ae9 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,17 +1,58 @@ class GitGarbageCollectWorker include Sidekiq::Worker - include Gitlab::ShellAdapter include DedicatedSidekiqQueue + include Gitlab::CurrentSettings sidekiq_options retry: false - def perform(project_id) + def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) project = Project.find(project_id) + task = task.to_sym + + cmd = command(task) + repo_path = project.repository.path_to_repo + description = "'#{cmd.join(' ')}' in #{repo_path}" + + Gitlab::GitLogger.info(description) + + output, status = Gitlab::Popen.popen(cmd, repo_path) + Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero? - gitlab_shell.gc(project.repository_storage_path, project.path_with_namespace) # Refresh the branch cache in case garbage collection caused a ref lookup to fail + flush_ref_caches(project) if task == :gc + ensure + Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? + end + + private + + def command(task) + case task + when :gc + git(write_bitmaps: bitmaps_enabled?) + %w[gc] + when :full_repack + git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects] + when :incremental_repack + # Normal git repack fails when bitmaps are enabled. It is impossible to + # create a bitmap here anyway. + git(write_bitmaps: false) + %w[repack -d] + else + raise "Invalid gc task: #{task.inspect}" + end + end + + def flush_ref_caches(project) project.repository.after_create_branch project.repository.branch_names project.repository.has_visible_content? end + + def bitmaps_enabled? + current_application_settings.housekeeping_bitmaps_enabled + end + + def git(write_bitmaps:) + config_value = write_bitmaps ? 'true' : 'false' + %W[git -c repack.writeBitmaps=#{config_value}] + end end diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb new file mode 100644 index 00000000000..cdb860b6675 --- /dev/null +++ b/app/workers/pipeline_notification_worker.rb @@ -0,0 +1,12 @@ +class PipelineNotificationWorker + include Sidekiq::Worker + include PipelineQueue + + def perform(pipeline_id, recipients = nil) + pipeline = Ci::Pipeline.find_by(id: pipeline_id) + + return unless pipeline + + NotificationService.new.pipeline_finished(pipeline, recipients) + end +end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb new file mode 100644 index 00000000000..071741fbacd --- /dev/null +++ b/app/workers/process_commit_worker.rb @@ -0,0 +1,67 @@ +# Worker for processing individiual commit messages pushed to a repository. +# +# Jobs for this worker are scheduled for every commit that is being pushed. As a +# result of this the workload of this worker should be kept to a bare minimum. +# Consider using an extra worker if you need to add any extra (and potentially +# slow) processing of commits. +class ProcessCommitWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + # project_id - The ID of the project this commit belongs to. + # user_id - The ID of the user that pushed the commit. + # commit_sha - The SHA1 of the commit to process. + # default - The data was pushed to the default branch. + def perform(project_id, user_id, commit_sha, default = false) + project = Project.find_by(id: project_id) + + return unless project + + user = User.find_by(id: user_id) + + return unless user + + commit = find_commit(project, commit_sha) + + return unless commit + + author = commit.author || user + + process_commit_message(project, commit, user, author, default) + + update_issue_metrics(commit, author) + end + + def process_commit_message(project, commit, user, author, default = false) + closed_issues = default ? commit.closes_issues(user) : [] + + unless closed_issues.empty? + close_issues(project, user, author, commit, closed_issues) + end + + commit.create_cross_references!(author, closed_issues) + end + + def close_issues(project, user, author, commit, issues) + # We don't want to run permission related queries for every single issue, + # therefor we use IssueCollection here and skip the authorization check in + # Issues::CloseService#execute. + IssueCollection.new(issues).updatable_by_user(user).each do |issue| + Issues::CloseService.new(project, author). + close_issue(issue, commit: commit) + end + end + + def update_issue_metrics(commit, author) + mentioned_issues = commit.all_references(author).issues + + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). + update_all(first_mentioned_in_commit_at: commit.committed_date) + end + + private + + def find_commit(project, sha) + project.commit(sha) + end +end diff --git a/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml new file mode 100644 index 00000000000..8f03746ff80 --- /dev/null +++ b/changelogs/unreleased/20968-add-setting-to-check-unresolved-discussion.yml @@ -0,0 +1,4 @@ +--- +title: Add setting to only allow merge requests to be merged when all discussions are resolved +merge_request: 7125 +author: Rodolfo Arruda diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml new file mode 100644 index 00000000000..95d8fef1099 --- /dev/null +++ b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml @@ -0,0 +1,4 @@ +--- +title: Use the Gitlab Workhorse HTTP header in the admin dashboard +merge_request: +author: Chris Wright diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml new file mode 100644 index 00000000000..7b54d3df56d --- /dev/null +++ b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml @@ -0,0 +1,4 @@ +--- +title: Rewrite git blame spinach feature tests to rspec feature tests +merge_request: 7197 +author: Lisanne Fellinger diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml new file mode 100644 index 00000000000..c0afade9bc8 --- /dev/null +++ b/changelogs/unreleased/24255-search-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken commits search +merge_request: +author: diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml new file mode 100644 index 00000000000..3af4f5e677d --- /dev/null +++ b/changelogs/unreleased/add-api-label-id.yml @@ -0,0 +1,4 @@ +--- +title: Expose label IDs in API +merge_request: 7275 +author: Rares Sfirlogea diff --git a/changelogs/unreleased/add-project-import-data-index.yml b/changelogs/unreleased/add-project-import-data-index.yml new file mode 100644 index 00000000000..f5e4005f544 --- /dev/null +++ b/changelogs/unreleased/add-project-import-data-index.yml @@ -0,0 +1,4 @@ +--- +title: Add an index for project_id in project_import_data to improve performance +merge_request: +author: diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml new file mode 100644 index 00000000000..85b6c2761bb --- /dev/null +++ b/changelogs/unreleased/api-label-priorities.yml @@ -0,0 +1,4 @@ +--- +title: API: Ability to retrieve version information +merge_request: 7286 +author: Robert Schilling diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml new file mode 100644 index 00000000000..d132d7e79c3 --- /dev/null +++ b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml @@ -0,0 +1,4 @@ +--- +title: Return 400 when creating a system hook fails +merge_request: 7350 +author: Robert Schilling diff --git a/changelogs/unreleased/broken-link-frontend-dev-guide.yml b/changelogs/unreleased/broken-link-frontend-dev-guide.yml new file mode 100644 index 00000000000..d7b6f4a7701 --- /dev/null +++ b/changelogs/unreleased/broken-link-frontend-dev-guide.yml @@ -0,0 +1,4 @@ +--- +title: Fix broken link to observatory cli on Frontend Dev Guide +merge_request: +author: Sam Rose diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml new file mode 100644 index 00000000000..e29a9f34ed4 --- /dev/null +++ b/changelogs/unreleased/faster_project_search.yml @@ -0,0 +1,4 @@ +--- +title: Faster search inside Project +merge_request: +author: diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml new file mode 100644 index 00000000000..d1bc8ea2eb1 --- /dev/null +++ b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml @@ -0,0 +1,4 @@ +--- +title: Fix 404 on network page when entering non-existent git revision +merge_request: 7172 +author: Hiroyuki Sato diff --git a/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml new file mode 100644 index 00000000000..8b41063151b --- /dev/null +++ b/changelogs/unreleased/fix-uncheckable-label-for-force_remove_source_branch.yml @@ -0,0 +1,4 @@ +--- +title: Clicking "force remove source branch" label now toggles the checkbox again +merge_request: +author: diff --git a/changelogs/unreleased/git-gc-improvements.yml b/changelogs/unreleased/git-gc-improvements.yml new file mode 100644 index 00000000000..f15e667ce87 --- /dev/null +++ b/changelogs/unreleased/git-gc-improvements.yml @@ -0,0 +1,4 @@ +--- +title: Finer-grained Git gargage collection +merge_request: 6588 +author: diff --git a/changelogs/unreleased/issue_23032.yml b/changelogs/unreleased/issue_23032.yml new file mode 100644 index 00000000000..d376cf52112 --- /dev/null +++ b/changelogs/unreleased/issue_23032.yml @@ -0,0 +1,4 @@ +--- +title: Allow to test JIRA service settings without having a repository +merge_request: +author: diff --git a/changelogs/unreleased/ldap_check_bind.yml b/changelogs/unreleased/ldap_check_bind.yml new file mode 100644 index 00000000000..daff8103a07 --- /dev/null +++ b/changelogs/unreleased/ldap_check_bind.yml @@ -0,0 +1,4 @@ +--- +title: Introduce better credential and error checking to `rake gitlab:ldap:check` +merge_request: 6601 +author: diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml new file mode 100644 index 00000000000..b43060674b2 --- /dev/null +++ b/changelogs/unreleased/pipeline-notifications.yml @@ -0,0 +1,6 @@ +--- +title: Add CI notifications. Who triggered a pipeline would receive an email after + the pipeline is succeeded or failed. Users could also update notification settings + accordingly +merge_request: 6342 +author: diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml new file mode 100644 index 00000000000..9f596e6a584 --- /dev/null +++ b/changelogs/unreleased/process-commits-using-sidekiq.yml @@ -0,0 +1,4 @@ +--- +title: Process commits using a dedicated Sidekiq worker +merge_request: 6802 +author: diff --git a/changelogs/unreleased/remove-heading-space-from-diff-content.yml b/changelogs/unreleased/remove-heading-space-from-diff-content.yml new file mode 100644 index 00000000000..1ea85784d29 --- /dev/null +++ b/changelogs/unreleased/remove-heading-space-from-diff-content.yml @@ -0,0 +1,4 @@ +--- +title: Remove an extra leading space from diff paste data +merge_request: 7133 +author: Hiroyuki Sato diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml new file mode 100644 index 00000000000..17cd5a993dd --- /dev/null +++ b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml @@ -0,0 +1,4 @@ +--- +title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2 +merge_request: +author: diff --git a/changelogs/unreleased/show-status-from-branch.yml b/changelogs/unreleased/show-status-from-branch.yml new file mode 100644 index 00000000000..1afc230c05c --- /dev/null +++ b/changelogs/unreleased/show-status-from-branch.yml @@ -0,0 +1,4 @@ +--- +title: Fix showing pipeline status for a given commit from correct branch +merge_request: 7034 +author: diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml new file mode 100644 index 00000000000..3df2a415dbc --- /dev/null +++ b/changelogs/unreleased/sidekiq_default_retries.yml @@ -0,0 +1,4 @@ +--- +title: Set default Sidekiq retries to 3 +merge_request: 7294 +author: diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml new file mode 100644 index 00000000000..ddb266ba558 --- /dev/null +++ b/changelogs/unreleased/upgrade-timeago.yml @@ -0,0 +1,4 @@ +--- +title: Replace jQuery.timeago with timeago.js +merge_request: 6274 +author: ClemMakesApps diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml new file mode 100644 index 00000000000..e498f8dd0a6 --- /dev/null +++ b/changelogs/unreleased/use-separate-token-for-incoming-email.yml @@ -0,0 +1,4 @@ +--- +title: Use separate email-token for incoming email and revert back the inactive feature +merge_request: 5914 +author: diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb new file mode 100644 index 00000000000..25003cf0239 --- /dev/null +++ b/config/initializers/routing_draw.rb @@ -0,0 +1,7 @@ +# Adds draw method into Rails routing +# It allows us to keep routing splitted into files +class ActionDispatch::Routing::Mapper + def draw(routes_name) + instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) + end +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 0455a98dbfe..023af2af23c 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -2,6 +2,9 @@ redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE +# Default is to retry 25 times with exponential backoff. That's too much. +Sidekiq.default_worker_options = { retry: 3 } + Sidekiq.configure_server do |config| config.redis = redis_config_hash diff --git a/config/routes.rb b/config/routes.rb index 659ea51bc75..7bf6c03e69b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,12 +2,6 @@ require 'sidekiq/web' require 'sidekiq/cron/web' require 'api/api' -class ActionDispatch::Routing::Mapper - def draw(routes_name) - instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) - end -end - Rails.application.routes.draw do concern :access_requestable do post :request_access, on: :collection diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb new file mode 100644 index 00000000000..03adc4815f3 --- /dev/null +++ b/config/routes/git_http.rb @@ -0,0 +1,37 @@ +scope constraints: { id: /.+\.git/, format: nil } do + # Git HTTP clients ('git clone' etc.) + get '/info/refs', to: 'git_http#info_refs' + post '/git-upload-pack', to: 'git_http#git_upload_pack' + post '/git-receive-pack', to: 'git_http#git_receive_pack' + + # Git LFS API (metadata) + post '/info/lfs/objects/batch', to: 'lfs_api#batch' + post '/info/lfs/objects', to: 'lfs_api#deprecated' + get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' + + # GitLab LFS object storage + scope constraints: { oid: /[a-f0-9]{64}/ } do + get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' + + scope constraints: { size: /[0-9]+/ } do + put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' + put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' + end + end +end + +# Allow /info/refs, /info/refs?service=git-upload-pack, and +# /info/refs?service=git-receive-pack, but nothing else. +# +git_http_handshake = lambda do |request| + request.query_string.blank? || + request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) +end + +ref_redirect = redirect do |params, request| + path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" + path << "?#{request.query_string}" unless request.query_string.blank? + path +end + +get '/info/refs', constraints: git_http_handshake, to: ref_redirect diff --git a/config/routes/group.rb b/config/routes/group.rb index 826048ba196..3c392f77ef6 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -3,7 +3,7 @@ require 'constraints/group_url_constrainer' constraints(GroupUrlConstrainer.new) do scope(path: ':id', as: :group, - constraints: { id: /[a-zA-Z.0-9_\-]+(? 'groups#show', as: :group_canonical +scope(path: 'groups/:id', controller: :groups) do + get :edit, as: :edit_group + get :issues, as: :issues_group + get :merge_requests, as: :merge_requests_group + get :projects, as: :projects_group + get :activity, as: :activity_group end + +scope(path: 'groups/:group_id', module: :groups, as: :group) do + resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do + post :resend_invite, on: :member + delete :leave, on: :collection + end + + resource :avatar, only: [:destroy] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resources :labels, except: [:show], constraints: { id: /\d+/ } +end + +# Must be last route in this file +get 'groups/:id' => 'groups#show', as: :group_canonical diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 4cb68c9b34a..52b9a565db8 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -4,6 +4,7 @@ resource :profile, only: [:show, :update] do get :applications, to: 'oauth/applications#index' put :reset_private_token + put :reset_incoming_email_token put :update_username end diff --git a/config/routes/project.rb b/config/routes/project.rb index 8142e231621..82defb0ba71 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -18,152 +18,17 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: get :autocomplete_sources get :activity get :refs + put :new_issue_address end scope module: :projects do - scope constraints: { id: /.+\.git/, format: nil } do - # Git HTTP clients ('git clone' etc.) - get '/info/refs', to: 'git_http#info_refs' - post '/git-upload-pack', to: 'git_http#git_upload_pack' - post '/git-receive-pack', to: 'git_http#git_receive_pack' - - # Git LFS API (metadata) - post '/info/lfs/objects/batch', to: 'lfs_api#batch' - post '/info/lfs/objects', to: 'lfs_api#deprecated' - get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated' - - # GitLab LFS object storage - scope constraints: { oid: /[a-f0-9]{64}/ } do - get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download' - - scope constraints: { size: /[0-9]+/ } do - put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize' - put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize' - end - end - end - - # Allow /info/refs, /info/refs?service=git-upload-pack, and - # /info/refs?service=git-receive-pack, but nothing else. - # - git_http_handshake = lambda do |request| - request.query_string.blank? || - request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) - end - - ref_redirect = redirect do |params, request| - path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" - path << "?#{request.query_string}" unless request.query_string.blank? - path - end - - get '/info/refs', constraints: git_http_handshake, to: ref_redirect - - # Blob routes: - get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' - post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' - get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' - put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' - post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' + draw :git_http # # Templates # get '/templates/:template_type/:key' => 'templates#show', as: :template - scope do - get( - '/blob/*id/diff', - to: 'blob#diff', - constraints: { id: /.+/, format: false }, - as: :blob_diff - ) - get( - '/blob/*id', - to: 'blob#show', - constraints: { id: /.+/, format: false }, - as: :blob - ) - delete( - '/blob/*id', - to: 'blob#destroy', - constraints: { id: /.+/, format: false } - ) - put( - '/blob/*id', - to: 'blob#update', - constraints: { id: /.+/, format: false } - ) - post( - '/blob/*id', - to: 'blob#create', - constraints: { id: /.+/, format: false } - ) - end - - scope do - get( - '/raw/*id', - to: 'raw#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :raw - ) - end - - scope do - get( - '/tree/*id', - to: 'tree#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :tree - ) - end - - scope do - get( - '/find_file/*id', - to: 'find_file#show', - constraints: { id: /.+/, format: /html/ }, - as: :find_file - ) - end - - scope do - get( - '/files/*id', - to: 'find_file#list', - constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, - as: :files - ) - end - - scope do - post( - '/create_dir/*id', - to: 'tree#create_dir', - constraints: { id: /.+/ }, - as: 'create_dir' - ) - end - - scope do - get( - '/blame/*id', - to: 'blame#show', - constraints: { id: /.+/, format: /(html|js)/ }, - as: :blame - ) - end - - scope do - get( - '/commits/*id', - to: 'commits#show', - constraints: { id: /.+/, format: false }, - as: :commits - ) - end - resource :avatar, only: [:show, :destroy] resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do member do @@ -206,29 +71,6 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end - WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID - - scope do - # Order matters to give priority to these matches - get '/wikis/git_access', to: 'wikis#git_access' - get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' - post '/wikis', to: 'wikis#create' - - get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID - get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID - - get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID - delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID - put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID - post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown' - end - - resource :repository, only: [:create] do - member do - get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } - end - end - resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do member do get :test @@ -245,23 +87,6 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: resources :forks, only: [:index, :new, :create] resource :import, only: [:new, :create, :show] - resources :refs, only: [] do - collection do - get 'switch' - end - - member do - # tree viewer logs - get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } - # Directories with leading dots erroneously get rejected if git - # ref regex used in constraints. Regex verification now done in controller. - get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { - id: /.*/, - path: /.*/ - } - end - end - resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do member do get :commits @@ -467,6 +292,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: end end end + + # Since both wiki and repository routing contains wildcard characters + # its preferable to keep it below all other project routes + draw :wiki + draw :repository end end end diff --git a/config/routes/repository.rb b/config/routes/repository.rb new file mode 100644 index 00000000000..76dcf113aea --- /dev/null +++ b/config/routes/repository.rb @@ -0,0 +1,110 @@ +# All routing related to repositoty browsing + +resource :repository, only: [:create] do + member do + get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + end +end + +resources :refs, only: [] do + collection do + get 'switch' + end + + member do + # tree viewer logs + get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + # Directories with leading dots erroneously get rejected if git + # ref regex used in constraints. Regex verification now done in controller. + get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: { + id: /.*/, + path: /.*/ + } + end +end + +get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' +post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' +get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' +put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' +post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' + +scope do + get( + '/blob/*id/diff', + to: 'blob#diff', + constraints: { id: /.+/, format: false }, + as: :blob_diff + ) + get( + '/blob/*id', + to: 'blob#show', + constraints: { id: /.+/, format: false }, + as: :blob + ) + delete( + '/blob/*id', + to: 'blob#destroy', + constraints: { id: /.+/, format: false } + ) + put( + '/blob/*id', + to: 'blob#update', + constraints: { id: /.+/, format: false } + ) + post( + '/blob/*id', + to: 'blob#create', + constraints: { id: /.+/, format: false } + ) + + get( + '/raw/*id', + to: 'raw#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :raw + ) + + get( + '/tree/*id', + to: 'tree#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :tree + ) + + get( + '/find_file/*id', + to: 'find_file#show', + constraints: { id: /.+/, format: /html/ }, + as: :find_file + ) + + get( + '/files/*id', + to: 'find_file#list', + constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }, + as: :files + ) + + post( + '/create_dir/*id', + to: 'tree#create_dir', + constraints: { id: /.+/ }, + as: 'create_dir' + ) + + get( + '/blame/*id', + to: 'blame#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :blame + ) + + # File/dir history + get( + '/commits/*id', + to: 'commits#show', + constraints: { id: /.+/, format: false }, + as: :commits + ) +end diff --git a/config/routes/user.rb b/config/routes/user.rb index 0a9c924863d..dc1068af6f6 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -14,31 +14,32 @@ end constraints(UserUrlConstrainer.new) do scope(path: ':username', as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(?"gin_trgm_ops"} + add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index bf7814875bf..fd23047f027 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -35,6 +35,10 @@ of one hour. To enable LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. +There is a Rake task to check LDAP configuration. After configuring LDAP +using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check) +for information on the LDAP check Rake task. + >**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to one GitLab server. diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index ad1fa98b63b..f846c06ca42 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -3,6 +3,14 @@ > [Introduced][ce-2371] in GitLab 8.4. --- +## Automatic housekeeping + +GitLab automatically runs `git gc` and `git repack` on repositories +after Git pushes. If needed you can change how often this happens, or +to turn it off, go to **Admin area > Settings** +(`/admin/application_settings`). + +## Manual housekeeping The housekeeping function runs `git gc` ([man page][man]) on the current project Git repository. diff --git a/doc/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png similarity index 100% rename from doc/raketasks/check_repos_output.png rename to doc/administration/img/raketasks/check_repos_output.png diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md new file mode 100644 index 00000000000..d1d2fed4861 --- /dev/null +++ b/doc/administration/raketasks/check.md @@ -0,0 +1,97 @@ +# Check Rake Tasks + +## Repository Integrity + +Even though Git is very resilient and tries to prevent data integrity issues, +there are times when things go wrong. The following Rake tasks intend to +help GitLab administrators diagnose problem repositories so they can be fixed. + +There are 3 things that are checked to determine integrity. + +1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)). + This step verifies the connectivity and validity of objects in the repository. +1. Check for `config.lock` in the repository directory. +1. Check for any branch/references lock files in `refs/heads`. + +It's important to note that the existence of `config.lock` or reference locks +alone do not necessarily indicate a problem. Lock files are routinely created +and removed as Git and GitLab perform operations on the repository. They serve +to prevent data integrity issues. However, if a Git operation is interrupted these +locks may not be cleaned up properly. + +The following symptoms may indicate a problem with repository integrity. If users +experience these symptoms you may use the rake tasks described below to determine +exactly which repositories are causing the trouble. + +- Receiving an error when trying to push code - `remote: error: cannot lock ref` +- A 500 error when viewing the GitLab dashboard or when accessing a specific project. + +### Check all GitLab repositories + +This task loops through all repositories on the GitLab server and runs the +3 integrity checks described previously. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:repo:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production +``` + +### Check repositories for a specific user + +This task checks all repositories that a specific user has access to. This is important +because sometimes you know which user is experiencing trouble but you don't know +which project might be the cause. + +If the rake task is executed without brackets at the end, you will be prompted +to enter a username. + +**Omnibus Installation** + +```bash +sudo gitlab-rake gitlab:user:check_repos +sudo gitlab-rake gitlab:user:check_repos[] +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:user:check_repos[] RAILS_ENV=production +``` + +Example output: + +![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png) + +## LDAP Check + +The LDAP check Rake task will test the bind_dn and password credentials +(if configured) and will list a sample of LDAP users. This task is also +executed as part of the `gitlab:check` task, but can run independently +using the command below. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:ldap:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production +``` + +By default, the task will return a sample of 100 LDAP users. Change this +limit by passing a number to the check task: + +```bash +rake gitlab:ldap:check[50] +``` diff --git a/doc/api/labels.md b/doc/api/labels.md index 656232cc940..78686fdcad4 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -20,46 +20,61 @@ Example response: ```json [ - { - "name" : "bug", - "color" : "#d9534f", - "description": "Bug reported by user", - "open_issues_count": 1, - "closed_issues_count": 0, - "open_merge_requests_count": 1 - }, - { - "color" : "#d9534f", - "name" : "confirmed", - "description": "Confirmed issue", - "open_issues_count": 2, - "closed_issues_count": 5, - "open_merge_requests_count": 0 - }, - { - "name" : "critical", - "color" : "#d9534f", - "description": "Critical issue. Need fix ASAP", - "open_issues_count": 1, - "closed_issues_count": 3, - "open_merge_requests_count": 1 - }, - { - "name" : "documentation", - "color" : "#f0ad4e", - "description": "Issue about documentation", - "open_issues_count": 1, - "closed_issues_count": 0, - "open_merge_requests_count": 2 - }, - { - "color" : "#5cb85c", - "name" : "enhancement", - "description": "Enhancement proposal", - "open_issues_count": 1, - "closed_issues_count": 0, - "open_merge_requests_count": 1 - } + { + "id" : 1, + "name" : "bug", + "color" : "#d9534f", + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1, + "subscribed": false, + "priority": 10 + }, + { + "id" : 4, + "color" : "#d9534f", + "name" : "confirmed", + "description": "Confirmed issue", + "open_issues_count": 2, + "closed_issues_count": 5, + "open_merge_requests_count": 0, + "subscribed": false, + "priority": null + }, + { + "id" : 7, + "name" : "critical", + "color" : "#d9534f", + "description": "Critical issue. Need fix ASAP", + "open_issues_count": 1, + "closed_issues_count": 3, + "open_merge_requests_count": 1, + "subscribed": false, + "priority": null + }, + { + "id" : 8, + "name" : "documentation", + "color" : "#f0ad4e", + "description": "Issue about documentation", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 2, + "subscribed": false, + "priority": null + }, + { + "id" : 9, + "color" : "#5cb85c", + "name" : "enhancement", + "description": "Enhancement proposal", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1, + "subscribed": true, + "priority": null + } ] ``` @@ -80,6 +95,7 @@ POST /projects/:id/labels | `name` | string | yes | The name of the label | | `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign | | `description` | string | no | The description of the label | +| `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. | ```bash curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" @@ -89,9 +105,15 @@ Example response: ```json { - "name" : "feature", - "color" : "#5843AD", - "description":null + "id" : 10, + "name" : "feature", + "color" : "#5843AD", + "description":null, + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false, + "priority": null } ``` @@ -120,14 +142,15 @@ Example response: ```json { - "title" : "feature", - "color" : "#5843AD", - "description": "New feature proposal", - "updated_at" : "2015-11-03T21:22:30.737Z", - "template" : false, - "project_id" : 1, - "created_at" : "2015-11-03T21:22:30.737Z", - "id" : 9 + "id" : 1, + "name" : "bug", + "color" : "#d9534f", + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1, + "subscribed": false, + "priority": null } ``` @@ -151,6 +174,8 @@ PUT /projects/:id/labels | `new_name` | string | yes if `color` is not provided | The new name of the label | | `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign | | `description` | string | no | The new description of the label | +| `priority` | integer | no | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. | + ```bash curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels" @@ -160,9 +185,15 @@ Example response: ```json { - "color" : "#8E44AD", - "name" : "docs", - "description": "Documentation" + "id" : 8, + "name" : "docs", + "color" : "#8E44AD", + "description": "Documentation", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 2, + "subscribed": false, + "priority": null } ``` @@ -191,13 +222,15 @@ Example response: ```json { - "name": "Docs", - "color": "#cc0033", - "description": "", - "open_issues_count": 0, - "closed_issues_count": 0, - "open_merge_requests_count": 0, - "subscribed": true + "id" : 1, + "name" : "bug", + "color" : "#d9534f", + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1, + "subscribed": true, + "priority": null } ``` @@ -226,12 +259,14 @@ Example response: ```json { - "name": "Docs", - "color": "#cc0033", - "description": "", - "open_issues_count": 0, - "closed_issues_count": 0, - "open_merge_requests_count": 0, - "subscribed": false + "id" : 1, + "name" : "bug", + "color" : "#d9534f", + "description": "Bug reported by user", + "open_issues_count": 1, + "closed_issues_count": 0, + "open_merge_requests_count": 1, + "subscribed": false, + "priority": null } ``` diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index ff6c9e4931c..aea1c12a392 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -4,7 +4,7 @@ **Valid notification levels** -The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized: +The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized: ``` disabled @@ -28,6 +28,8 @@ reopen_merge_request close_merge_request reassign_merge_request merge_merge_request +failed_pipeline +success_pipeline ``` ## Global notification settings @@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab | `close_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification | | `merge_merge_request` | boolean | no | Enable/disable this notification | +| `failed_pipeline` | boolean | no | Enable/disable this notification | +| `success_pipeline` | boolean | no | Enable/disable this notification | Example response: @@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab | `close_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification | | `merge_merge_request` | boolean | no | Enable/disable this notification | +| `failed_pipeline` | boolean | no | Enable/disable this notification | +| `success_pipeline` | boolean | no | Enable/disable this notification | Example responses: @@ -161,7 +167,9 @@ Example responses: "reopen_merge_request": false, "close_merge_request": false, "reassign_merge_request": false, - "merge_merge_request": false + "merge_merge_request": false, + "failed_pipeline": false, + "success_pipeline": false } } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 4f4b20a1874..bbb3bfb4995 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -89,6 +89,7 @@ Parameters: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false }, { @@ -151,6 +152,7 @@ Parameters: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ] @@ -429,6 +431,7 @@ Parameters: } ], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ``` @@ -602,6 +605,7 @@ Parameters: | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -634,6 +638,7 @@ Parameters: | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -665,6 +670,7 @@ Parameters: | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | +| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | @@ -752,6 +758,7 @@ Example response: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ``` @@ -820,6 +827,7 @@ Example response: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ``` @@ -908,6 +916,7 @@ Example response: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ``` @@ -996,6 +1005,7 @@ Example response: "public_builds": true, "shared_with_groups": [], "only_allow_merge_if_build_succeeds": false, + "only_allow_merge_if_all_discussions_are_resolved": false, "request_access_enabled": false } ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 959741f7338..89088cf9b83 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user. 2. Install Docker Engine on server. - For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/). + For more information how to install Docker Engine on different systems + checkout the [Supported installations](https://docs.docker.com/engine/installation/). 3. Add `gitlab-runner` user to `docker` group: @@ -122,11 +123,17 @@ In order to do that, follow the steps: Insecure = false ``` -1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service): +1. You can now use `docker` in the build script (note the inclusion of the + `docker:dind` service): ```yaml image: docker:latest + # When using dind, it's wise to use the overlayfs driver for + # improved performance. + variables: + DOCKER_DRIVER: overlay + services: - docker:dind @@ -140,15 +147,21 @@ In order to do that, follow the steps: - docker run my-docker-image /script/to/run/tests ``` -Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges: -* By enabling `--docker-privileged`, you are effectively disabling all of -the security mechanisms of containers and exposing your host to privilege -escalation which can lead to container breakout. For more information, check out the official Docker documentation on -[Runtime privilege and Linux capabilities][docker-cap]. -* Using docker-in-docker, each build is in a clean environment without the past -history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers. -* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form -offered. +Docker-in-Docker works well, and is the recommended configuration, but it is +not without its own challenges: + +- By enabling `--docker-privileged`, you are effectively disabling all of + the security mechanisms of containers and exposing your host to privilege + escalation which can lead to container breakout. For more information, check + out the official Docker documentation on + [Runtime privilege and Linux capabilities][docker-cap]. +- Using docker-in-docker, each build is in a clean environment without the past + history. Concurrent builds work fine because every build gets it's own + instance of Docker engine so they won't conflict with each other. But this + also means builds can be slower because there's no caching of layers. +- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest + form offered. To use a different driver, see + [Using the overlayfs driver](#using-the-overlayfs-driver). An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker. @@ -221,6 +234,40 @@ work as expected since volume mounting is done in the context of the host machine, not the build container. e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests` +## Using the OverlayFS driver + +By default, when using `docker:dind`, Docker uses the `vfs` storage driver which +copies the filesystem on every run. This is a very disk-intensive operation +which can be avoided if a different driver is used, for example `overlay`. + +1. Make sure a recent kernel is used, preferably `>= 4.2`. +1. Check whether the `overlay` module is loaded: + + ``` + sudo lsmod | grep overlay + ``` + + If you see no result, then it isn't loaded. To load it use: + + ``` + sudo modprobe overlay + ``` + + If everything went fine, you need to make sure module is loaded on reboot. + On Ubuntu systems, this is done by editing `/etc/modules`. Just add the + following line into it: + + ``` + overlay + ``` + +1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`: + + ``` + variables: + DOCKER_DRIVER: overlay + ``` + ## Using the GitLab Container Registry > **Note:** diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 1d7d9127a64..ec8f2d6531c 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -228,7 +228,7 @@ For our currently-supported browsers, see our [requirements][requirements]. [page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 [chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools [audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules -[observatory-cli]: https://github.com/mozilla/http-observatory-cli) +[observatory-cli]: https://github.com/mozilla/http-observatory-cli [qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html [secure_headers]: https://github.com/twitter/secureheaders [mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 2574c2c0472..bbcd26477f3 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -66,6 +66,12 @@ producing errors whenever it tries to use the `dummy` column. As a result of the above downtime _is_ required when removing a column, even when using PostgreSQL. +## Renaming Columns + +Renaming columns requires downtime as running GitLab instances will continue +using the old column name until a new version is deployed. This can result +in the instance producing errors, which in turn can impact the user experience. + ## Changing Column Constraints Generally changing column constraints requires checking all rows in the table to diff --git a/doc/install/installation.md b/doc/install/installation.md index 7e947e4b2ba..b5e2640b380 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -403,7 +403,7 @@ If you are not using Linux you may have to run `gmake` instead of cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.8.5 + sudo -u git -H git checkout v1.0.0 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/integration/README.md b/doc/integration/README.md index a928b74f9b8..c2fd299db07 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [JIRA](jira.md) Integrate with the JIRA issue tracker +- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure diff --git a/doc/integration/jira.md b/doc/integration/jira.md index 2e31fd994de..78aa6634116 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,197 +1,3 @@ # GitLab JIRA integration -GitLab can be configured to interact with JIRA. Configuration happens via -user name and password. Connecting to a JIRA server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance, see the -[configuration](#configuration) section. If you have one JIRA instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. - -Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab. - -## Table of Contents -* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues) -* [Closing JIRA Issues from GitLab](#closing-JIRA-issues) -* [Configuration](#configuration) - -### Referencing JIRA Issues - -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the -format: - -``` - USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: - ENTITY_TITLE -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. -* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. -* `PROJECT_NAME` GitLab project name. -* `ENTITY_TITLE` Merge request title or commit message first line. - -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) - ---- - -### Closing JIRA Issues - -JIRA issues can be closed directly from GitLab by using trigger words, eg. -`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and -merge requests. When a commit which contains the trigger word in the commit -message is pushed, GitLab will add a comment in the mentioned JIRA issue. - -For example, for project named `PROJECT` in JIRA, we implemented a new feature -and created a merge request in GitLab. - -This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab -contains the improvement and in merge request description we say that this -merge request `Closes PROJECT-7` issue. - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) - ---- - -![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png) - ---- - -## Configuration - -### Configuring JIRA - -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. - -As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` -group. - -**It is important that the user `GitLab` has write-access to projects in JIRA** - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![jira create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access > View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users > GitLab user > Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - -JIRA configuration in GitLab is done via a project's **Services**. - -#### GitLab 8.13.0 with JIRA v1000.x - -To enable JIRA integration in a project, navigate to the project's -and open the context menu clicking on the top right gear icon, then go to -**Services > JIRA**. - -Fill in the required details on the page as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com | -| `Project key` | The short, all capital letter identifier for your JIRA project. | -| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -![JIRA service page](img/jira_service_page.png) - ---- - -#### GitLab 6.x-7.7 with JIRA v6.x - -_**Note:** GitLab versions 8.13.0 and up contain various integration improvements. -We strongly recommend upgrading._ - -In `gitlab.yml` enable the JIRA issue tracker section by -[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all -issues within GitLab are pointing to the JIRA issue tracker. - -After you set this, you will be able to close issues in JIRA by a commit in -GitLab. - -Go to your project's **Settings** page and fill in the project name for the -JIRA project: - -![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png) - ---- - -You can also enable the JIRA service that will allow you to interact with JIRA -issues. Go to the **Settings > Services > JIRA** and: - -1. Tick the active check box to enable the service -1. Supply the URL to JIRA server, for example http://JIRA.example.com -1. Supply the username of a user we created under `Configuring JIRA` section, - for example `gitlab` -1. Supply the password of the user -1. Optional: supply the JIRA API version, default is version `2` -1. Optional: supply the JIRA issue transition ID (issue transition to closed). - This is dependent on JIRA settings, default is `2` -1. Hit save - - -![JIRA services page](img/jira_service.png) - -[services-templates]: ../project_services/services_templates.md -[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115 +This document was moved under [project_services/jira](../project_services/jira.md). diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png index 440728795be..88943dc410e 100644 Binary files a/doc/project_services/img/builds_emails_service.png and b/doc/project_services/img/builds_emails_service.png differ diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png new file mode 100644 index 00000000000..aec472b9118 Binary files /dev/null and b/doc/project_services/img/jira_add_gitlab_commit_message.png differ diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png new file mode 100644 index 00000000000..0ba737bda9a Binary files /dev/null and b/doc/project_services/img/jira_add_user_to_group.png differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png new file mode 100644 index 00000000000..0609060cb05 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group.png differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png new file mode 100644 index 00000000000..53d77b17df0 Binary files /dev/null and b/doc/project_services/img/jira_create_new_group_name.png differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png new file mode 100644 index 00000000000..9eaa444ed25 Binary files /dev/null and b/doc/project_services/img/jira_create_new_user.png differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png new file mode 100644 index 00000000000..8d4657427ae Binary files /dev/null and b/doc/project_services/img/jira_group_access.png differ diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png new file mode 100644 index 00000000000..acdd83702d3 Binary files /dev/null and b/doc/project_services/img/jira_issue_closed.png differ diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png new file mode 100644 index 00000000000..1a2d9f04a6c Binary files /dev/null and b/doc/project_services/img/jira_issue_reference.png differ diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png new file mode 100644 index 00000000000..0703081d77b Binary files /dev/null and b/doc/project_services/img/jira_issues_workflow.png differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png new file mode 100644 index 00000000000..47785e3ba27 Binary files /dev/null and b/doc/project_services/img/jira_merge_request_close.png differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png new file mode 100644 index 00000000000..e785ec6140d Binary files /dev/null and b/doc/project_services/img/jira_project_name.png differ diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png new file mode 100644 index 00000000000..fb270d85e3c Binary files /dev/null and b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png new file mode 100644 index 00000000000..13aefce6f84 Binary files /dev/null and b/doc/project_services/img/jira_service.png differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png new file mode 100644 index 00000000000..eed69e80d2c Binary files /dev/null and b/doc/project_services/img/jira_service_close_issue.png differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png new file mode 100644 index 00000000000..a5b49c501ba Binary files /dev/null and b/doc/project_services/img/jira_service_page.png differ diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png new file mode 100644 index 00000000000..77630d39d39 Binary files /dev/null and b/doc/project_services/img/jira_submit_gitlab_merge_request.png differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png new file mode 100644 index 00000000000..5f002b59bac Binary files /dev/null and b/doc/project_services/img/jira_user_management_link.png differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png new file mode 100644 index 00000000000..937a50a77d9 Binary files /dev/null and b/doc/project_services/img/jira_workflow_screenshot.png differ diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 2ea1c58cb31..b626c746c79 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1 +1,246 @@ -GitLab JIRA integration documentation has moved to [here](../integration/jira.md). +# GitLab JIRA integration + +>**Note:** +Full JIRA integration was previously exclusive to GitLab Enterprise Edition. +With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce] +to GitLab Community Edition as well. + +--- + +GitLab can be configured to interact with [JIRA Core] either using an +on-premises instance or the SaaS solution that Atlassian offers. Configuration +happens via username and password on a per-project basis. Connecting to a JIRA +server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance or, in +case you have a single JIRA instance, you can pre-fill the JIRA service +settings page in GitLab with a default template. To configure the JIRA template, +see the [Services Templates documentation][services-templates]. + +Once the GitLab project is connected to JIRA, you can reference and close the +issues in JIRA directly from GitLab's merge requests. + +## Configuration + +The configuration consists of two parts: + +- [JIRA configuration](#configuring-jira) +- [GitLab configuration](#configuring-gitlab) + +### Configuring JIRA + +First things first, we need to create a user in JIRA which will have access to +all projects that need to integrate with GitLab. + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![JIRA create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access > View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users > GitLab user > Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +>**Note:** +The currently supported JIRA versions are v6.x and v7.x. and GitLab +7.8 or higher is required. + +--- + +Assuming you [have already configured JIRA](#configuring-jira), now it's time +to configure GitLab. + +JIRA configuration in GitLab is done via a project's +[**Services**](../project_services/project_services.md). + +To enable JIRA integration in a project, navigate to the project's +**Settings > Services > JIRA**. + +Fill in the required details on the page, as described in the table below. + +| Setting | Description | +| ------- | ----------- | +| `Description` | A name for the issue tracker (to differentiate between instances, for example). | +| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https:///issues/?jql=project=`. | +| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https:///browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. | +| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https:///secure/CreateIssue.jspa` | +| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https:///rest/api/2`. | +| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +For example, given the settings below: + +- the JIRA URL is `https://jira.example.com` +- the project is named `GITLAB` +- the user is named `gitlab` +- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans]) + +the following screenshot shows how the JIRA service settings should look like. + +![JIRA service page](img/jira_service_page.png) + +[trans]: img/jira_issues_workflow.png + +--- + +## JIRA issues + +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. + +### Referencing JIRA Issues + +If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link +which points back to JIRA is created. + +The same works for comments in merge requests as well. + +![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png) + +--- + +The mentioning action is two-fold, so a comment with a JIRA issue in GitLab +will automatically add a comment in that particular JIRA issue with the link +back to GitLab. + + +![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png) + +--- + +The comment on the JIRA issue is of the form: + +> USER mentioned this issue in LINK_TO_THE_MENTION + +Where: + +| Format | Description | +| ------ | ----------- | +| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. | +| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. | + +### Closing JIRA issues + +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). + +There are currently three trigger words, and you can use either one to achieve +the same goal: + +- `Resolves GITLAB-1` +- `Closes GITLAB-1` +- `Fixes GITLAB-1` + +where `GITLAB-1` the issue ID of the JIRA project. + +### JIRA issue closing example + +Let's say for example that we submitted a bug fix and created a merge request +in GitLab. The workflow would be something like this: + +1. Create a new branch +1. Fix the bug +1. Commit the changes and push branch to GitLab +1. Open a new merge request and reference the JIRA issue including one of the + trigger words, e.g.: `Fixes GITLAB-1`, in the description +1. Submit the merge request +1. Ask someone to review +1. Merge the merge request +1. The JIRA issue is automatically closed + +--- + +In the following screenshot you can see what the link references to the JIRA +issue look like. + +![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png) + +--- + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png) + +--- + +You can see from the above image that there are four references to GitLab: + +- The first is from a comment in a specific commit +- The second is from the JIRA issue reference in the merge request description +- The third is from the actual commit that solved the issue +- And the fourth is from the commit that the merge request created + +[services-templates]: ../project_services/services_templates.md "Services templates documentation" +[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" +[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" +[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" + +## Troubleshooting + +### GitLab is unable to comment on a ticket + +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition the +ticket, if you'd like GitLab to also take care of closing them. + +### GitLab is unable to close a ticket + +Make sure the the `Transition ID` you set within the JIRA settings matches the +one your project needs to close a ticket. diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 8116a1ce976..4442b7c1742 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome. | Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](../integration/jira.md) | JIRA issue tracker | +| [JIRA](jira.md) | JIRA issue tracker | | JetBrains TeamCity CI | A continuous integration and build server | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md index 3ff3fee6a40..f7f6a40cd04 100644 --- a/doc/raketasks/check.md +++ b/doc/raketasks/check.md @@ -1,63 +1,3 @@ # Check Rake Tasks -## Repository Integrity - -Even though Git is very resilient and tries to prevent data integrity issues, -there are times when things go wrong. The following Rake tasks intend to -help GitLab administrators diagnose problem repositories so they can be fixed. - -There are 3 things that are checked to determine integrity. - -1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)). - This step verifies the connectivity and validity of objects in the repository. -1. Check for `config.lock` in the repository directory. -1. Check for any branch/references lock files in `refs/heads`. - -It's important to note that the existence of `config.lock` or reference locks -alone do not necessarily indicate a problem. Lock files are routinely created -and removed as Git and GitLab perform operations on the repository. They serve -to prevent data integrity issues. However, if a Git operation is interrupted these -locks may not be cleaned up properly. - -The following symptoms may indicate a problem with repository integrity. If users -experience these symptoms you may use the rake tasks described below to determine -exactly which repositories are causing the trouble. - -- Receiving an error when trying to push code - `remote: error: cannot lock ref` -- A 500 error when viewing the GitLab dashboard or when accessing a specific project. - -### Check all GitLab repositories - -This task loops through all repositories on the GitLab server and runs the -3 integrity checks described previously. - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:repo:check - -# installation from source -bundle exec rake gitlab:repo:check RAILS_ENV=production -``` - -### Check repositories for a specific user - -This task checks all repositories that a specific user has access to. This is important -because sometimes you know which user is experiencing trouble but you don't know -which project might be the cause. - -If the rake task is executed without brackets at the end, you will be prompted -to enter a username. - -```bash -# omnibus-gitlab -sudo gitlab-rake gitlab:user:check_repos -sudo gitlab-rake gitlab:user:check_repos[] - -# installation from source -bundle exec rake gitlab:user:check_repos RAILS_ENV=production -bundle exec rake gitlab:user:check_repos[] RAILS_ENV=production -``` - -Example output: - -![gitlab:user:check_repos output](check_repos_output.png) +This document was moved to [administration/raketasks/check](../administration/raketasks/check.md). diff --git a/doc/university/README.md b/doc/university/README.md index 510b753f70d..49714e4fb59 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -200,7 +200,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project ## 4. External Articles -1. [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) +1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) 1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/) diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index 787511fd6cf..46ea19d11d0 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -84,7 +84,7 @@ GitLab 8.1. ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch --all -sudo -u git -H git checkout v0.8.5 +sudo -u git -H git checkout v1.0.0 sudo -u git -H make ``` diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png new file mode 100644 index 00000000000..52c8acf15e0 Binary files /dev/null and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved.png differ diff --git a/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png new file mode 100644 index 00000000000..79ba5c362c7 Binary files /dev/null and b/doc/user/project/merge_requests/img/only_allow_merge_if_all_discussions_are_resolved_msg.png differ diff --git a/doc/user/project/merge_requests/merge_request_discussion_resolution.md b/doc/user/project/merge_requests/merge_request_discussion_resolution.md index 2559f5f5250..285b1798ac5 100644 --- a/doc/user/project/merge_requests/merge_request_discussion_resolution.md +++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md @@ -33,7 +33,25 @@ resolved discussions tracker. !["3/4 discussions resolved"][discussions-resolved] +## Only allow merge requests to be merged if all discussions are resolved + +> [Introduced][ce-7125] in GitLab 8.14. + +You can prevent merge requests from being merged until all discussions are resolved. + +Navigate to your project's settings page, select the +**Only allow merge requests to be merged if all discussions are resolved** check +box and hit **Save** for the changes to take effect. + +![Only allow merge if all the discussions are resolved settings](img/only_allow_merge_if_all_discussions_are_resolved.png) + +From now on, you will not be able to merge from the UI until all discussions +are resolved. + +![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png) + [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 +[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md index c138061fd40..d4e5b5de685 100644 --- a/doc/user/project/merge_requests/merge_when_build_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md @@ -40,7 +40,7 @@ hit **Save** for the changes to take effect. ![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) -From now on, every time the pipelinefails you will not be able to merge the +From now on, every time the pipeline fails you will not be able to merge the merge request from the UI, until you make all relevant builds pass. -![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png) +![Only allow merge if build succeeds message](img/merge_when_build_succeeds_only_if_succeeds_msg.png) diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 1b49a5c385f..c936e8833c6 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -66,6 +66,7 @@ Below is the table of events users can be notified of: In all of the below cases, the notification will be sent to: - Participants: - the author and assignee of the issue/merge request + - the author of the pipeline - authors of comments on the issue/merge request - anyone mentioned by `@username` in the issue/merge request title or description - anyone mentioned by `@username` in any of the comments on the issue/merge request @@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to: | Reopen merge request | | | Merge merge request | | | New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | +| Failed pipeline | The above, plus the author of the pipeline | +| Successful pipeline | The above, plus the author of the pipeline | In addition, if the title or description of an Issue or Merge Request is diff --git a/features/profile/profile.feature b/features/profile/profile.feature index 447dd92a458..dc1339deb4c 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -59,11 +59,6 @@ Feature: Profile When I unsuccessfully change my password Then I should see a password error message - Scenario: I reset my token - Given I visit profile account page - Then I reset my token - And I should see new token - Scenario: I visit history tab Given I have activity When I visit Audit Log page diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature index 89a02706bd2..93c884e23c5 100644 --- a/features/project/network_graph.feature +++ b/features/project/network_graph.feature @@ -43,4 +43,4 @@ Feature: Project Network Graph Scenario: I should fail to look for a commit When I look for a commit by ";" - Then page status code should be 404 + Then I should see non-existent git revision error message diff --git a/features/project/source/git_blame.feature b/features/project/source/git_blame.feature deleted file mode 100644 index 48b1077dc6b..00000000000 --- a/features/project/source/git_blame.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Project Source Git Blame - Background: - Given I sign in as a user - And I own project "Shop" - Given I visit project source page - - Scenario: I blame file - Given I click on ".gitignore" file in repo - And I click Blame button - Then I should see git file blame diff --git a/features/snippets/public_snippets.feature b/features/snippets/public_snippets.feature deleted file mode 100644 index c2afb63b6d8..00000000000 --- a/features/snippets/public_snippets.feature +++ /dev/null @@ -1,10 +0,0 @@ -Feature: Public snippets - Scenario: Unauthenticated user should see public snippets - Given There is public "Personal snippet one" snippet - And I visit snippet page "Personal snippet one" - Then I should see snippet "Personal snippet one" - - Scenario: Unauthenticated user should see raw public snippets - Given There is public "Personal snippet one" snippet - And I visit snippet raw page "Personal snippet one" - Then I should see raw snippet "Personal snippet one" diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 0e81e99120b..0c88838767c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -117,7 +117,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I visit group "NonExistentGroup" page' do - visit group_path(-1) + visit group_path("NonExistentGroup") end step 'the archived project have some issues' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 05ab2a7dc73..ea480d2ad68 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -104,18 +104,6 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end end - step 'I reset my token' do - page.within '.private-token' do - @old_token = @user.private_token - click_button "Reset private token" - end - end - - step 'I should see new token' do - expect(find("#token").value).not_to eq @old_token - expect(find("#token").value).to eq @user.reload.private_token - end - step 'I have activity' do create(:closed_issue_event, author: current_user) end diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 019b3124a86..ff9251615c9 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -109,4 +109,8 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps find('button').click end end + + step 'I should see non-existent git revision error message' do + expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist." + end end diff --git a/features/steps/project/source/git_blame.rb b/features/steps/project/source/git_blame.rb deleted file mode 100644 index d0a27f47e2a..00000000000 --- a/features/steps/project/source/git_blame.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Spinach::Features::ProjectSourceGitBlame < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end - - step 'I click Blame button' do - click_link 'Blame' - end - - step 'I should see git file blame' do - expect(page).to have_content "*.rb" - expect(page).to have_content "Dmitriy Zaporozhets" - expect(page).to have_content "Initial commit" - end -end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 4df4e89f5b9..35b71599708 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -210,7 +210,7 @@ module SharedDiffNote end step 'I click side-by-side diff button' do - find('#parallel-diff-btn').click + find('#parallel-diff-btn').trigger('click') end step 'I see side-by-side diff button' do diff --git a/features/steps/snippets/public_snippets.rb b/features/steps/snippets/public_snippets.rb deleted file mode 100644 index 2ebdca5ed30..00000000000 --- a/features/steps/snippets/public_snippets.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Spinach::Features::PublicSnippets < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedSnippet - - step 'I should see snippet "Personal snippet one"' do - expect(page).to have_no_xpath("//i[@class='public-snippet']") - end - - step 'I should see raw snippet "Personal snippet one"' do - expect(page).to have_text(snippet.content) - end - - step 'I visit snippet page "Personal snippet one"' do - visit snippet_path(snippet) - end - - step 'I visit snippet raw page "Personal snippet one"' do - visit raw_snippet_path(snippet) - end - - def snippet - @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") - end -end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 1f378ba1635..1942aeea656 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -100,6 +100,7 @@ module API end expose :only_allow_merge_if_build_succeeds expose :request_access_enabled + expose :only_allow_merge_if_all_discussions_are_resolved end class Member < UserBasic @@ -432,11 +433,14 @@ module API end class LabelBasic < Grape::Entity - expose :name, :color, :description + expose :id, :name, :color, :description end class Label < LabelBasic expose :open_issues_count, :closed_issues_count, :open_merge_requests_count + expose :priority do |label, options| + label.priority(options[:project]) + end expose :subscribed do |label, options| label.subscribed?(options[:current_user]) diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 326e1e7ae00..97218054f37 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API success Entities::Label end get ':id/labels' do - present available_labels, with: Entities::Label, current_user: current_user + present available_labels, with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do @@ -21,17 +21,23 @@ module API requires :name, type: String, desc: 'The name of the label to be created' requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)" optional :description, type: String, desc: 'The description of label to be created' + optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true end post ':id/labels' do authorize! :admin_label, user_project - label = user_project.find_label(params[:name]) + label = available_labels.find_by(title: params[:name]) conflict!('Label already exists') if label - label = user_project.labels.create(declared(params, include_parent_namespaces: false).to_h) + priority = params.delete(:priority) + label_params = declared(params, + include_parent_namespaces: false, + include_missing: false).to_h + label = user_project.labels.create(label_params) if label.valid? - present label, with: Entities::Label, current_user: current_user + label.prioritize!(user_project, priority) if priority + present label, with: Entities::Label, current_user: current_user, project: user_project else render_validation_error!(label) end @@ -46,10 +52,10 @@ module API delete ':id/labels' do authorize! :admin_label, user_project - label = user_project.find_label(params[:name]) + label = user_project.labels.find_by(title: params[:name]) not_found!('Label') unless label - present label.destroy, with: Entities::Label, current_user: current_user + present label.destroy, with: Entities::Label, current_user: current_user, project: user_project end desc 'Update an existing label. At least one optional parameter is required.' do @@ -60,25 +66,34 @@ module API optional :new_name, type: String, desc: 'The new name of the label' optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)" optional :description, type: String, desc: 'The new description of label' - at_least_one_of :new_name, :color, :description + optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true + at_least_one_of :new_name, :color, :description, :priority end put ':id/labels' do authorize! :admin_label, user_project - label = user_project.find_label(params[:name]) + label = user_project.labels.find_by(title: params[:name]) not_found!('Label not found') unless label - update_params = declared(params, - include_parent_namespaces: false, - include_missing: false).to_h + update_priority = params.key?(:priority) + priority = params.delete(:priority) + label_params = declared(params, + include_parent_namespaces: false, + include_missing: false).to_h # Rename new name to the actual label attribute name - update_params['name'] = update_params.delete('new_name') if update_params.key?('new_name') + label_params[:name] = label_params.delete('new_name') if label_params.key?('new_name') - if label.update(update_params) - present label, with: Entities::Label, current_user: current_user - else - render_validation_error!(label) + render_validation_error!(label) unless label.update(label_params) + + if update_priority + if priority.nil? + label.unprioritize!(user_project) + else + label.prioritize!(user_project, priority) + end end + + present label, with: Entities::Label, current_user: current_user, project: user_project end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index da16e24d7ea..6b856128c2e 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -139,7 +139,8 @@ module API :shared_runners_enabled, :snippets_enabled, :visibility_level, - :wiki_enabled] + :wiki_enabled, + :only_allow_merge_if_all_discussions_are_resolved] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(current_user, attrs).execute if @project.saved? @@ -193,7 +194,8 @@ module API :shared_runners_enabled, :snippets_enabled, :visibility_level, - :wiki_enabled] + :wiki_enabled, + :only_allow_merge_if_all_discussions_are_resolved] attrs = map_public_to_visibility_level(attrs) @project = ::Projects::CreateService.new(user, attrs).execute if @project.saved? @@ -275,7 +277,8 @@ module API :shared_runners_enabled, :snippets_enabled, :visibility_level, - :wiki_enabled] + :wiki_enabled, + :only_allow_merge_if_all_discussions_are_resolved] attrs = map_public_to_visibility_level(attrs) authorize_admin_project authorize! :rename_project, user_project if attrs[:name].present? diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 32f731c5652..b6bfff9f20f 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -32,7 +32,7 @@ module API if hook.save present hook, with: Entities::Hook else - not_found! + render_validation_error!(hook) end end diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index 799b83b1069..80c844baecd 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -71,6 +71,14 @@ module Banzai @doc = parse_html(rinku) end + # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme + def contains_unsafe?(scheme) + return false unless scheme + + scheme = scheme.strip.downcase + Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) } + end + # Autolinks any text matching LINK_PATTERN that Rinku didn't already # replace def text_parse @@ -89,17 +97,27 @@ module Banzai doc end - def autolink_filter(text) - text.gsub(LINK_PATTERN) do |match| - # Remove any trailing HTML entities and store them for appending - # outside the link element. The entity must be marked HTML safe in - # order to be output literally rather than escaped. - match.gsub!(/((?:&[\w#]+;)+)\z/, '') - dropped = ($1 || '').html_safe - - options = link_options.merge(href: match) - content_tag(:a, match, options) + dropped + def autolink_match(match) + # start by stripping out dangerous links + begin + uri = Addressable::URI.parse(match) + return match if contains_unsafe?(uri.scheme) + rescue Addressable::URI::InvalidURIError + return match end + + # Remove any trailing HTML entities and store them for appending + # outside the link element. The entity must be marked HTML safe in + # order to be output literally rather than escaped. + match.gsub!(/((?:&[\w#]+;)+)\z/, '') + dropped = ($1 || '').html_safe + + options = link_options.merge(href: match) + content_tag(:a, match, options) + dropped + end + + def autolink_filter(text) + text.gsub(LINK_PATTERN) { |match| autolink_match(match) } end def link_options diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index f5d110e987b..d8a855ec1fe 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -63,12 +63,7 @@ module Banzai nodes.select do |node| if node.has_attribute?(project_attr) node_id = node.attr(project_attr).to_i - - if project && project.id == node_id - true - else - can?(user, :read_project, projects[node_id]) - end + can_read_reference?(user, projects[node_id]) else true end @@ -226,6 +221,15 @@ module Banzai attr_reader :current_user, :project + # When a feature is disabled or visible only for + # team members we should not allow team members + # see reference comments. + # Override this method on subclasses + # to check if user can read resource + def can_read_reference?(user, ref_project) + raise NotImplementedError + end + def lazy(&block) Gitlab::Lazy.new(&block) end diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb index 0fee9d267de..8c54a041cb8 100644 --- a/lib/banzai/reference_parser/commit_parser.rb +++ b/lib/banzai/reference_parser/commit_parser.rb @@ -29,6 +29,12 @@ module Banzai commits end + + private + + def can_read_reference?(user, ref_project) + can?(user, :download_code, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb index 69d01f8db15..0878b6afba3 100644 --- a/lib/banzai/reference_parser/commit_range_parser.rb +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -33,6 +33,12 @@ module Banzai range.valid_commits? ? range : nil end + + private + + def can_read_reference?(user, ref_project) + can?(user, :download_code, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb index a1264db2111..6e7b7669578 100644 --- a/lib/banzai/reference_parser/external_issue_parser.rb +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -20,6 +20,12 @@ module Banzai def issue_ids_per_project(nodes) gather_attributes_per_project(nodes, self.class.data_attribute) end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_issue, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb index e5d1eb11d7f..aa76c64ac5f 100644 --- a/lib/banzai/reference_parser/label_parser.rb +++ b/lib/banzai/reference_parser/label_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Label end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_label, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index c9a9ca79c09..40451947e6c 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation MergeRequest.includes(:author, :assignee, :target_project) end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_merge_request, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb index a000ac61e5c..d3968d6b229 100644 --- a/lib/banzai/reference_parser/milestone_parser.rb +++ b/lib/banzai/reference_parser/milestone_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Milestone end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_milestone, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb index fa71b3c952a..63b592137bb 100644 --- a/lib/banzai/reference_parser/snippet_parser.rb +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -6,6 +6,12 @@ module Banzai def references_relation Snippet end + + private + + def can_read_reference?(user, ref_project) + can?(user, :read_project_snippet, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 863f5725d3b..7adaffa19c1 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -30,22 +30,36 @@ module Banzai nodes.each do |node| if node.has_attribute?(group_attr) - node_group = groups[node.attr(group_attr).to_i] - - if node_group && - can?(user, :read_group, node_group) - visible << node - end - # Remaining nodes will be processed by the parent class' - # implementation of this method. + next unless can_read_group_reference?(node, user, groups) + visible << node + elsif can_read_project_reference?(node) + visible << node else remaining << node end end + # If project does not belong to a group + # and does not have the same project id as the current project + # base class will check if user can read the project that contains + # the user reference. visible + super(current_user, remaining) end + # Check if project belongs to a group which + # user can read. + def can_read_group_reference?(node, user, groups) + node_group = groups[node.attr('data-group').to_i] + + node_group && can?(user, :read_group, node_group) + end + + def can_read_project_reference?(node) + node_id = node.attr('data-project').to_i + + project && project.id == node_id + end + def nodes_user_can_reference(current_user, nodes) project_attr = 'data-project' author_attr = 'data-author' @@ -88,6 +102,10 @@ module Banzai collection_objects_for_ids(Project, ids). flat_map { |p| p.team.members.to_a } end + + def can_read_reference?(user, ref_project) + can?(user, :read_project, ref_project) + end end end end diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index ce048a36fa0..f31fb6c3f71 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -46,7 +46,7 @@ module Banzai return html if html.present? html = cacheless_render_field(object, field) - object.update_column(html_field, html) unless object.new_record? || object.destroyed? + update_object(object, html_field, html) unless object.new_record? || object.destroyed? html end @@ -166,5 +166,9 @@ module Banzai return unless cache_key Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name)) end + + def update_object(object, html_field, html) + object.update_column(html_field, html) + end end end diff --git a/lib/constraints/constrainer_helper.rb b/lib/constraints/constrainer_helper.rb new file mode 100644 index 00000000000..ab07a6793d9 --- /dev/null +++ b/lib/constraints/constrainer_helper.rb @@ -0,0 +1,15 @@ +module ConstrainerHelper + def extract_resource_path(path) + id = path.dup + id.sub!(/\A#{relative_url_root}/, '') if relative_url_root + id.sub(/\A\/+/, '').sub(/\/+\z/, '').sub(/.atom\z/, '') + end + + private + + def relative_url_root + if defined?(Gitlab::Application.config.relative_url_root) + Gitlab::Application.config.relative_url_root + end + end +end diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index ca39b1961ae..2af6e1a11c8 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -1,7 +1,15 @@ -require 'constraints/namespace_url_constrainer' +require_relative 'constrainer_helper' -class GroupUrlConstrainer < NamespaceUrlConstrainer - def find_resource(id) - Group.find_by_path(id) +class GroupUrlConstrainer + include ConstrainerHelper + + def matches?(request) + id = extract_resource_path(request.path) + + if id =~ Gitlab::Regex.namespace_regex + Group.find_by(path: id).present? + else + false + end end end diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb deleted file mode 100644 index 91b70143f11..00000000000 --- a/lib/constraints/namespace_url_constrainer.rb +++ /dev/null @@ -1,24 +0,0 @@ -class NamespaceUrlConstrainer - def matches?(request) - id = request.path - id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root - id = id.sub(/\A\/+/, '').split('/').first - id = id.sub(/.atom\z/, '') if id - - if id =~ Gitlab::Regex.namespace_regex - find_resource(id) - end - end - - def find_resource(id) - Namespace.find_by_path(id) - end - - private - - def relative_url_root - if defined?(Gitlab::Application.config.relative_url_root) - Gitlab::Application.config.relative_url_root - end - end -end diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb index 504a0f5d93e..4d722ad5af2 100644 --- a/lib/constraints/user_url_constrainer.rb +++ b/lib/constraints/user_url_constrainer.rb @@ -1,7 +1,15 @@ -require 'constraints/namespace_url_constrainer' +require_relative 'constrainer_helper' -class UserUrlConstrainer < NamespaceUrlConstrainer - def find_resource(id) - User.find_by('lower(username) = ?', id.downcase) +class UserUrlConstrainer + include ConstrainerHelper + + def matches?(request) + id = extract_resource_path(request.path) + + if id =~ Gitlab::Regex.namespace_regex + User.find_by('lower(username) = ?', id.downcase).present? + else + false + end end end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 9b74364849e..82551f1f222 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -106,7 +106,7 @@ module ExtractsPath # resolved (e.g., when a user inserts an invalid path or ref). def assign_ref_vars # assign allowed options - allowed_options = ["filter_ref", "extended_sha1"] + allowed_options = ["filter_ref"] @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } @options = HashWithIndifferentAccess.new(@options) @@ -114,17 +114,13 @@ module ExtractsPath @ref, @path = extract_ref(@id) @repo = @project.repository - if @options[:extended_sha1].present? - @commit = @repo.commit(@options[:extended_sha1]) - else + @commit = @repo.commit(@ref) + + if @path.empty? && !@commit && @id.ends_with?('.atom') + @id = @ref = extract_ref_without_atom(@id) @commit = @repo.commit(@ref) - if @path.empty? && !@commit && @id.ends_with?('.atom') - @id = @ref = extract_ref_without_atom(@id) - @commit = @repo.commit(@ref) - - request.format = :atom if @commit - end + request.format = :atom if @commit end raise InvalidPathError unless @commit diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 9cec71a3222..82e194c1af1 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -127,19 +127,6 @@ module Gitlab 'rm-project', storage, "#{name}.git"]) end - # Gc repository - # - # storage - project storage path - # path - project path with namespace - # - # Ex. - # gc("/path/to/storage", "gitlab/gitlab-ci") - # - def gc(storage, path) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc', - storage, "#{path}.git"]) - end - # Add new key to gitlab-shell # # Ex. diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index b164f5a2eea..7e3d5647b39 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -1,45 +1,44 @@ module Gitlab class ContributionsCalendar - attr_reader :activity_dates, :projects, :user + attr_reader :contributor + attr_reader :current_user + attr_reader :projects - def initialize(projects, user) - @projects = projects - @user = user + def initialize(contributor, current_user = nil) + @contributor = contributor + @current_user = current_user + @projects = ContributedProjectsFinder.new(contributor).execute(current_user) end def activity_dates return @activity_dates if @activity_dates.present? - @activity_dates = {} + # Can't use Event.contributions here because we need to check 3 different + # project_features for the (currently) 3 different contribution types date_from = 1.year.ago + repo_events = event_counts(date_from, :repository). + having(action: Event::PUSHED) + issue_events = event_counts(date_from, :issues). + having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue") + mr_events = event_counts(date_from, :merge_requests). + having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest") - events = Event.reorder(nil).contributions.where(author_id: user.id). - where("created_at > ?", date_from).where(project_id: projects). - group('date(created_at)'). - select('date(created_at) as date, count(id) as total_amount'). - map(&:attributes) + union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events]) + events = Event.find_by_sql(union.to_sql).map(&:attributes) - activity_dates = (1.year.ago.to_date..Date.today).to_a - - activity_dates.each do |date| - day_events = events.find { |day_events| day_events["date"] == date } - - if day_events - @activity_dates[date] = day_events["total_amount"] - end + @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities| + activities[event["date"]] += event["total_amount"] end - - @activity_dates end def events_by_date(date) - events = Event.contributions.where(author_id: user.id). - where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). + events = Event.contributions.where(author_id: contributor.id). + where(created_at: date.beginning_of_day..date.end_of_day). where(project_id: projects) - events.select do |event| - event.push? || event.issue? || event.merge_request? - end + # Use visible_to_user? instead of the complicated logic in activity_dates + # because we're only viewing the events for a single day. + events.select {|event| event.visible_to_user?(current_user) } end def starting_year @@ -49,5 +48,30 @@ module Gitlab def starting_month Date.today.month end + + private + + def event_counts(date_from, feature) + t = Event.arel_table + + # re-running the contributed projects query in each union is expensive, so + # use IN(project_ids...) instead. It's the intersection of two users so + # the list will be (relatively) short + @contributed_project_ids ||= projects.uniq.pluck(:id) + authed_projects = Project.where(id: @contributed_project_ids). + with_feature_available_for_user(feature, current_user). + reorder(nil). + select(:id) + + conditions = t[:created_at].gteq(date_from.beginning_of_day). + and(t[:created_at].lteq(Date.today.end_of_day)). + and(t[:author_id].eq(contributor.id)) + + Event.reorder(nil). + select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount'). + group(t[:project_id], t[:target_type], t[:action], 'date(created_at)'). + where(conditions). + having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) + end end end diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb index 5cf9d5ebe28..bd3267e2a80 100644 --- a/lib/gitlab/email/handler.rb +++ b/lib/gitlab/email/handler.rb @@ -4,8 +4,7 @@ require 'gitlab/email/handler/create_issue_handler' module Gitlab module Email module Handler - # The `CreateIssueHandler` feature is disabled for the time being. - HANDLERS = [CreateNoteHandler] + HANDLERS = [CreateNoteHandler, CreateIssueHandler] def self.for(mail, mail_key) HANDLERS.find do |klass| diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 4e6566af8ab..9f90a3ec2b2 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -5,16 +5,16 @@ module Gitlab module Email module Handler class CreateIssueHandler < BaseHandler - attr_reader :project_path, :authentication_token + attr_reader :project_path, :incoming_email_token def initialize(mail, mail_key) super(mail, mail_key) - @project_path, @authentication_token = + @project_path, @incoming_email_token = mail_key && mail_key.split('+', 2) end def can_handle? - !authentication_token.nil? + !incoming_email_token.nil? end def execute @@ -29,7 +29,7 @@ module Gitlab end def author - @author ||= User.find_by(authentication_token: authentication_token) + @author ||= User.find_by(incoming_email_token: incoming_email_token) end def project diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 7e8f35e9298..2dd42704396 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -1,66 +1,52 @@ +require 'securerandom' + module Gitlab # This class implements an 'exclusive lease'. We call it a 'lease' # because it has a set expiry time. We call it 'exclusive' because only # one caller may obtain a lease for a given key at a time. The # implementation is intended to work across GitLab processes and across - # servers. It is a 'cheap' alternative to using SQL queries and updates: + # servers. It is a cheap alternative to using SQL queries and updates: # you do not need to change the SQL schema to start using # ExclusiveLease. # - # It is important to choose the timeout wisely. If the timeout is very - # high (1 hour) then the throughput of your operation gets very low (at - # most once an hour). If the timeout is lower than how long your - # operation may take then you cannot count on exclusivity. For example, - # if the timeout is 10 seconds and you do an operation which may take 20 - # seconds then two overlapping operations may hold a lease for the same - # key at the same time. - # - # This class has no 'cancel' method. I originally decided against adding - # it because it would add complexity and a false sense of security. The - # complexity: instead of setting '1' we would have to set a UUID, and to - # delete it we would have to execute Lua on the Redis server to only - # delete the key if the value was our own UUID. Otherwise there is a - # chance that when you intend to cancel your lease you actually delete - # someone else's. The false sense of security: you cannot design your - # system to rely too much on the lease being cancelled after use because - # the calling (Ruby) process may crash or be killed. You _cannot_ count - # on begin/ensure blocks to cancel a lease, because the 'ensure' does - # not always run. Think of 'kill -9' from the Unicorn master for - # instance. - # - # If you find that leases are getting in your way, ask yourself: would - # it be enough to lower the lease timeout? Another thing that might be - # appropriate is to only use a lease for bulk/automated operations, and - # to ignore the lease when you get a single 'manual' user request (a - # button click). - # class ExclusiveLease - def initialize(key, timeout:) - @key, @timeout = key, timeout + LUA_CANCEL_SCRIPT = <<-EOS + local key, uuid = KEYS[1], ARGV[1] + if redis.call("get", key) == uuid then + redis.call("del", key) + end + EOS + + def self.cancel(key, uuid) + Gitlab::Redis.with do |redis| + redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_key(key)], argv: [uuid]) + end end - # Try to obtain the lease. Return true on success, + def self.redis_key(key) + "gitlab:exclusive_lease:#{key}" + end + + def initialize(key, timeout:) + @redis_key = self.class.redis_key(key) + @timeout = timeout + @uuid = SecureRandom.uuid + end + + # Try to obtain the lease. Return lease UUID on success, # false if the lease is already taken. def try_obtain # Performing a single SET is atomic Gitlab::Redis.with do |redis| - !!redis.set(redis_key, '1', nx: true, ex: @timeout) + redis.set(@redis_key, @uuid, nx: true, ex: @timeout) && @uuid end end # Returns true if the key for this lease is set. def exists? Gitlab::Redis.with do |redis| - redis.exists(redis_key) + redis.exists(@redis_key) end end - - # No #cancel method. See comments above! - - private - - def redis_key - "gitlab:exclusive_lease:#{@key}" - end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 799794c0171..bcbf6455998 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -2,8 +2,18 @@ # class return an instance of `GitlabAccessStatus` module Gitlab class GitAccess + UnauthorizedError = Class.new(StandardError) + + ERROR_MESSAGES = { + upload: 'You are not allowed to upload code for this project.', + download: 'You are not allowed to download code from this project.', + deploy_key: 'Deploy keys are not allowed to push code.', + no_repo: 'A repository for this project does not exist yet.' + } + DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } PUSH_COMMANDS = %w{ git-receive-pack } + ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities @@ -16,56 +26,43 @@ module Gitlab end def check(cmd, changes) - return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed? - - unless actor - return build_status_object(false, "No user or key was provided.") - end - - if user && !user_access.allowed? - return build_status_object(false, "Your account has been blocked.") - end - - unless project && (user_access.can_read_project? || deploy_key_can_read_project?) - return build_status_object(false, 'The project you were looking for could not be found.') - end + check_protocol! + check_active_user! + check_project_accessibility! + check_command_existence!(cmd) case cmd when *DOWNLOAD_COMMANDS download_access_check when *PUSH_COMMANDS push_access_check(changes) - else - build_status_object(false, "The command you're trying to execute is not allowed.") end + + build_status_object(true) + rescue UnauthorizedError => ex + build_status_object(false, ex.message) end def download_access_check if user user_download_access_check - elsif deploy_key - build_status_object(true) - else - raise 'Wrong actor' + elsif deploy_key.nil? && !Guest.can?(:download_code, project) + raise UnauthorizedError, ERROR_MESSAGES[:download] end end def push_access_check(changes) if user user_push_access_check(changes) - elsif deploy_key - build_status_object(false, "Deploy keys are not allowed to push code.") else - raise 'Wrong actor' + raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload] end end def user_download_access_check unless user_can_download_code? || build_can_download_code? - return build_status_object(false, "You are not allowed to download code from this project.") + raise UnauthorizedError, ERROR_MESSAGES[:download] end - - build_status_object(true) end def user_can_download_code? @@ -78,15 +75,15 @@ module Gitlab def user_push_access_check(changes) unless authentication_abilities.include?(:push_code) - return build_status_object(false, "You are not allowed to upload code for this project.") + raise UnauthorizedError, ERROR_MESSAGES[:upload] end if changes.blank? - return build_status_object(true) + return # Allow access. end unless project.repository.exists? - return build_status_object(false, "A repository for this project does not exist yet.") + raise UnauthorizedError, ERROR_MESSAGES[:no_repo] end changes_list = Gitlab::ChangesList.new(changes) @@ -96,11 +93,9 @@ module Gitlab status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push - return status + raise UnauthorizedError, status.message end end - - build_status_object(true) end def change_access_check(change) @@ -113,6 +108,30 @@ module Gitlab private + def check_protocol! + unless protocol_allowed? + raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed" + end + end + + def check_active_user! + if user && !user_access.allowed? + raise UnauthorizedError, "Your account has been blocked." + end + end + + def check_project_accessibility! + if project.blank? || !can_read_project? + raise UnauthorizedError, 'The project you were looking for could not be found.' + end + end + + def check_command_existence!(cmd) + unless ALL_COMMANDS.include?(cmd) + raise UnauthorizedError, "The command you're trying to execute is not allowed." + end + end + def matching_merge_request?(newrev, branch_name) Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? end @@ -130,6 +149,16 @@ module Gitlab end end + def can_read_project? + if user + user_access.can_read_project? + elsif deploy_key + deploy_key_can_read_project? + else + Guest.can?(:read_project, project) + end + end + protected def user diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index ecc28799737..90cf38a8513 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -52,13 +52,14 @@ module Gitlab fetch_resources(:labels, repo, per_page: 100) do |labels| labels.each do |raw| begin - label = LabelFormatter.new(project, raw).create! - @labels[label.title] = label.id + LabelFormatter.new(project, raw).create! rescue => e errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message } end end end + + cache_labels! end def import_milestones @@ -234,6 +235,12 @@ module Gitlab end end + def cache_labels! + project.labels.select(:id, :title).find_each do |label| + @labels[label.title] = label.id + end + end + def fetch_resources(resource_type, *opts) return if imported?(resource_type) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index d7be50bd437..801dfde9a36 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -1,5 +1,7 @@ module Gitlab module IncomingEmail + WILDCARD_PLACEHOLDER = '%{key}'.freeze + class << self FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze @@ -7,8 +9,16 @@ module Gitlab config.enabled && config.address end + def supports_wildcard? + config.address && config.address.include?(WILDCARD_PLACEHOLDER) + end + + def supports_issue_creation? + enabled? && supports_wildcard? + end + def reply_address(key) - config.address.gsub('%{key}', key) + config.address.gsub(WILDCARD_PLACEHOLDER, key) end def key_from_address(address) diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index f9bb5775323..6ea069d26df 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -92,6 +92,10 @@ module Gitlab options['timeout'].to_i end + def has_auth? + options['password'] || options['bind_dn'] + end + protected def base_config @@ -122,10 +126,6 @@ module Gitlab } } end - - def has_auth? - options['password'] || options['bind_dn'] - end end end end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 24733435a5a..b8326a64b22 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -5,11 +5,7 @@ module Gitlab def initialize(current_user, project, query, repository_ref = nil) @current_user = current_user @project = project - @repository_ref = if repository_ref.present? - repository_ref - else - nil - end + @repository_ref = repository_ref.presence @query = query end @@ -47,33 +43,31 @@ module Gitlab private def blobs - if project.empty_repo? || query.blank? - [] - else - project.repository.search_files(query, repository_ref) - end + @blobs ||= project.repository.search_files(query, repository_ref) end def wiki_blobs - if project.wiki_enabled? && query.present? - project_wiki = ProjectWiki.new(project) + @wiki_blobs ||= begin + if project.wiki_enabled? && query.present? + project_wiki = ProjectWiki.new(project) - unless project_wiki.empty? - project_wiki.search_files(query) + unless project_wiki.empty? + project_wiki.search_files(query) + else + [] + end else [] end - else - [] end end def notes - project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') + @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') end def commits - project.repository.find_commits_by_message(query) + @commits ||= project.repository.find_commits_by_message(query) end def project_ids_relation diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 0d30e1bb92e..cb1659f9cee 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -8,6 +8,10 @@ module Gitlab @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze end + def namespace_route_regex + @namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze + end + def namespace_regex_message "can contain only letters, digits, '_', '-' and '.'. " \ "Cannot start with '-' or end in '.', '.git' or '.atom'." \ diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 2ae48a970ce..35c4194e87c 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -760,7 +760,7 @@ namespace :gitlab do end namespace :ldap do - task :check, [:limit] => :environment do |t, args| + task :check, [:limit] => :environment do |_, args| # Only show up to 100 results because LDAP directories can be very big. # This setting only affects the `rake gitlab:check` script. args.with_defaults(limit: 100) @@ -768,7 +768,7 @@ namespace :gitlab do start_checking "LDAP" if Gitlab::LDAP::Config.enabled? - print_users(args.limit) + check_ldap(args.limit) else puts 'LDAP is disabled in config/gitlab.yml' end @@ -776,21 +776,42 @@ namespace :gitlab do finished_checking "LDAP" end - def print_users(limit) - puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" - + def check_ldap(limit) servers = Gitlab::LDAP::Config.providers servers.each do |server| puts "Server: #{server}" - Gitlab::LDAP::Adapter.open(server) do |adapter| - users = adapter.users(adapter.config.uid, '*', limit) - users.each do |user| - puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + + begin + Gitlab::LDAP::Adapter.open(server) do |adapter| + check_ldap_auth(adapter) + + puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)" + + users = adapter.users(adapter.config.uid, '*', limit) + users.each do |user| + puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}" + end end + rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e + puts "Could not connect to the LDAP server: #{e.message}".color(:red) end end end + + def check_ldap_auth(adapter) + auth = adapter.config.has_auth? + + if auth && adapter.ldap.bind + message = 'Success'.color(:green) + elsif auth + message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red) + else + message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow) + end + + puts "LDAP authentication... #{message}" + end end namespace :repo do diff --git a/package.json b/package.json index a303c9c1eac..e75e070451b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "eslint-config-airbnb": "^12.0.0", "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.0.1", + "eslint-plugin-jasmine": "^1.8.1", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.4.1" } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 940d54f8686..49127aecc63 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -297,6 +297,72 @@ describe Projects::MergeRequestsController do end end end + + describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do + let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) } + + context 'when enabled' do + before do + project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true) + end + + context 'with unresolved discussion' do + before do + expect(merge_request).not_to be_discussions_resolved + end + + it 'returns :failed' do + merge_with_sha + + expect(assigns(:status)).to eq(:failed) + end + end + + context 'with all discussions resolved' do + before do + merge_request.discussions.each { |d| d.resolve!(user) } + expect(merge_request).to be_discussions_resolved + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + end + end + + context 'when disabled' do + before do + project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false) + end + + context 'with unresolved discussion' do + before do + expect(merge_request).not_to be_discussions_resolved + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + end + + context 'with all discussions resolved' do + before do + merge_request.discussions.each { |d| d.resolve!(user) } + expect(merge_request).to be_discussions_resolved + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + end + end + end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 8eefa284ba0..5ddcaa60dc6 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -7,6 +7,26 @@ describe ProjectsController do let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + describe 'GET index' do + context 'as a user' do + it 'redirects to root page' do + sign_in(user) + + get :index + + expect(response).to redirect_to(root_path) + end + end + + context 'as a guest' do + it 'redirects to Explore page' do + get :index + + expect(response).to redirect_to(explore_root_path) + end + end + end + describe "GET show" do context "user not project member" do before { sign_in(user) } @@ -264,6 +284,33 @@ describe ProjectsController do end end + describe 'PUT #new_issue_address' do + subject do + put :new_issue_address, + namespace_id: project.namespace.to_param, + id: project.to_param + user.reload + end + + before do + sign_in(user) + project.team << [user, :developer] + allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) + end + + it 'has http status 200' do + expect(response).to have_http_status(200) + end + + it 'changes the user incoming email token' do + expect { subject }.to change { user.incoming_email_token } + end + + it 'changes projects new issue address' do + expect { subject }.to change { project.new_issue_address(user) } + end + end + describe "POST #toggle_star" do it "toggles star if user is signed in" do sign_in(user) diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index f780e01253c..37eb49c94df 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -68,6 +68,11 @@ FactoryGirl.define do factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:reopened] factory :merge_request_with_diffs, traits: [:with_diffs] + factory :merge_request_with_diff_notes do + after(:create) do |mr| + create(:diff_note_on_merge_request, noteable: mr, project: mr.source_project) + end + end factory :labeled_merge_request do transient do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index dd4a86b1e31..bfd88a254f1 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -49,13 +49,17 @@ FactoryGirl.define do end after(:create) do |project, evaluator| + # Builds and MRs can't have higher visibility level than repository access level. + builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min + merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min + project.project_feature. - update_attributes( + update_attributes!( wiki_access_level: evaluator.wiki_access_level, - builds_access_level: evaluator.builds_access_level, + builds_access_level: builds_access_level, snippets_access_level: evaluator.snippets_access_level, issues_access_level: evaluator.issues_access_level, - merge_requests_access_level: evaluator.merge_requests_access_level, + merge_requests_access_level: merge_requests_access_level, repository_access_level: evaluator.repository_access_level ) end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index a92075fec8f..6cb8753e8fc 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -380,6 +380,25 @@ describe 'Issue Boards', feature: true, js: true do wait_for_board_cards(1, 5) end + + it 'creates new list from a new label' do + click_button 'Create new list' + + wait_for_ajax + + click_link 'Create new label' + + fill_in('new_label_name', with: 'Testing New Label') + + first('.suggest-colors a').click + + click_button 'Create' + + wait_for_ajax + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 5) + end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 338c53f08a6..44646ffc602 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -12,11 +12,15 @@ describe 'Commits' do end let!(:pipeline) do - FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha + create(:ci_pipeline, + project: project, + ref: project.default_branch, + sha: project.commit.sha, + status: :success) end context 'commit status is Generic Commit Status' do - let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } + let!(:status) { create(:generic_commit_status, pipeline: pipeline) } before do project.team << [@user, :reporter] @@ -39,7 +43,7 @@ describe 'Commits' do end context 'commit status is Ci Build' do - let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline } + let!(:build) { create(:ci_build, pipeline: pipeline) } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do @@ -48,13 +52,22 @@ describe 'Commits' do end describe 'Project commits' do + let!(:pipeline_from_other_branch) do + create(:ci_pipeline, + project: project, + ref: 'fix', + sha: project.commit.sha, + status: :failed) + end + before do visit namespace_project_commits_path(project.namespace, project, :master) end - it 'shows build status' do + it 'shows correct build status from default branch' do page.within("//li[@id='commit-#{pipeline.short_sha}']") do - expect(page).to have_css(".ci-status-link") + expect(page).to have_css('.ci-status-link') + expect(page).to have_css('.ci-status-icon-success') end end end diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb new file mode 100644 index 00000000000..f6409e00f22 --- /dev/null +++ b/spec/features/global_search_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'Global search', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + project.team << [user, :master] + login_with(user) + end + + describe 'I search through the issues and I see pagination' do + before do + allow_any_instance_of(Gitlab::SearchResults).to receive(:per_page).and_return(1) + create_list(:issue, 2, project: project, title: 'initial') + end + + it "has a pagination" do + visit dashboard_projects_path + + fill_in "search", with: "initial" + click_button "Go" + + select_filter("Issues") + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + end +end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb new file mode 100644 index 00000000000..476eca17a9d --- /dev/null +++ b/spec/features/groups/issues_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +feature 'Group issues page', feature: true do + let(:path) { issues_group_path(group) } + let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} + + include_examples 'project features apply to issuables', Issue +end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb new file mode 100644 index 00000000000..a2791b57544 --- /dev/null +++ b/spec/features/groups/merge_requests_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +feature 'Group merge requests page', feature: true do + let(:path) { merge_requests_group_path(group) } + let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")} + + include_examples 'project features apply to issuables', MergeRequest +end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index fb0c4704285..ab901e74617 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -18,22 +18,24 @@ feature 'Start new branch from an issue', feature: true do end context "when there is a referenced merge request" do - let(:note) do - create(:note, :on_issue, :system, project: project, + let!(:note) do + create(:note, :on_issue, :system, project: project, noteable: issue, note: "Mentioned in !#{referenced_mr.iid}") end + let(:referenced_mr) do create(:merge_request, :simple, source_project: project, target_project: project, description: "Fixes ##{issue.iid}", author: user) end before do - issue.notes << note + referenced_mr.cache_merge_request_closes_issues!(user) visit namespace_project_issue_path(project.namespace, project, issue) end it "hides the new branch button", js: true do + expect(page).to have_css('#new-branch .unavailable') expect(page).not_to have_css('#new-branch .available') expect(page).to have_content /1 Related Merge Request/ end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index b504329656f..cdd02a8c8e3 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'Issues', feature: true do include IssueHelpers include SortingHelper + include WaitForAjax let(:project) { create(:project) } @@ -368,6 +369,26 @@ describe 'Issues', feature: true do end end + describe 'when I want to reset my incoming email token' do + let(:project1) { create(:project, namespace: @user.namespace) } + + before do + allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) + project1.team << [@user, :master] + visit namespace_project_issues_path(@user.namespace, project1) + end + + it 'changes incoming email address token', js: true do + find('.issue-email-modal-btn').click + previous_token = find('input#issue_email').value + + find('.incoming-email-token-reset').click + wait_for_ajax + + expect(find('input#issue_email').value).not_to eq(previous_token) + end + end + describe 'update labels from issue#show', js: true do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } let!(:label) { create(:label, project: project) } @@ -553,7 +574,7 @@ describe 'Issues', feature: true do end end - xdescribe 'new issue by email' do + describe 'new issue by email' do shared_examples 'show the email in the modal' do before do stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb new file mode 100644 index 00000000000..7f11db3c417 --- /dev/null +++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +feature 'Check if mergeable with unresolved discussions', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) } + + before do + login_as user + project.team << [user, :master] + end + + context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do + before do + project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true) + end + + context 'with unresolved discussions' do + it 'does not allow to merge' do + visit_merge_request(merge_request) + + expect(page).not_to have_button 'Accept Merge Request' + expect(page).to have_content('This merge request has unresolved discussions') + end + end + + context 'with all discussions resolved' do + before do + merge_request.discussions.each { |d| d.resolve!(user) } + end + + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + end + + context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do + before do + project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false) + end + + context 'with unresolved discussions' do + it 'does not allow to merge' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + + context 'with all discussions resolved' do + before do + merge_request.discussions.each { |d| d.resolve!(user) } + end + + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + end + + def visit_merge_request(merge_request) + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index c3d8c349ca4..7a562b5e03d 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -32,4 +32,33 @@ describe 'Profile account page', feature: true do expect(current_path).to eq(profile_account_path) end end + + describe 'when I reset private token' do + before do + visit profile_account_path + end + + it 'resets private token' do + previous_token = find("#private-token").value + + click_link('Reset private token') + + expect(find('#private-token').value).not_to eq(previous_token) + end + end + + describe 'when I reset incoming email token' do + before do + allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) + visit profile_account_path + end + + it 'resets incoming email token' do + previous_token = find('#incoming-email-token').value + + click_link('Reset incoming email token') + + expect(find('#incoming-email-token').value).not_to eq(previous_token) + end + end end diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb new file mode 100644 index 00000000000..69295e450d0 --- /dev/null +++ b/spec/features/projects/files/browse_files_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'user checks git blame', feature: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_tree_path(project.namespace, project, project.default_branch) + end + + scenario "can see blame of '.gitignore'" do + click_link ".gitignore" + click_link 'Blame' + + expect(page).to have_content "*.rb" + expect(page).to have_content "Dmitriy Zaporozhets" + expect(page).to have_content "Initial commit" + end +end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index b3ba40b35af..472491188c9 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -22,8 +22,20 @@ feature 'Ref switcher', feature: true, js: true do input.native.send_keys :down input.native.send_keys :down input.native.send_keys :enter - - expect(page).to have_content 'expand-collapse-files' end + + expect(page).to have_title 'expand-collapse-files' + end + + it "user selects ref with special characters" do + click_button 'master' + wait_for_ajax + + page.within '.project-refs-form' do + page.fill_in 'Search branches and tags', with: "'test'" + click_link "'test'" + end + + expect(page).to have_title "'test'" end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 1806200c82c..caecd027aaa 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -100,6 +100,32 @@ describe "Search", feature: true do expect(page).to have_link(snippet.title) end + + it 'finds a commit' do + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: 'add' + click_button 'Go' + end + + click_link "Commits" + + expect(page).to have_selector('.commit-row-description') + end + + it 'finds a code' do + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: 'def' + click_button 'Go' + end + + click_link "Code" + + expect(page).to have_selector('.file-content .code') + end end describe 'Right header search field', feature: true do diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb index db53a9cec97..49deacc5c74 100644 --- a/spec/features/security/project/snippet/internal_access_spec.rb +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Internal Project Snippets Access", feature: true do include AccessMatchers - let(:project) { create(:project, :internal) } + let(:project) { create(:empty_project, :internal) } let(:owner) { project.owner } let(:master) { create(:user) } @@ -48,31 +48,63 @@ describe "Internal Project Snippets Access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/snippets/:id for an internal snippet" do - subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + describe "GET /:project_path/snippets/:id" do + context "for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + context "for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end end - describe "GET /:project_path/snippets/:id for a private snippet" do - subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + describe "GET /:project_path/snippets/:id/raw" do + context "for an internal snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + context "for a private snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end end end diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb index d23d645c8e5..a1bfc076d99 100644 --- a/spec/features/security/project/snippet/private_access_spec.rb +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Private Project Snippets Access", feature: true do include AccessMatchers - let(:project) { create(:project, :private) } + let(:project) { create(:empty_project, :private) } let(:owner) { project.owner } let(:master) { create(:user) } @@ -60,4 +60,18 @@ describe "Private Project Snippets Access", feature: true do it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end + + describe "GET /:project_path/snippets/:id/raw for a private snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end end diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb index e3665b6116a..30bcd87ef04 100644 --- a/spec/features/security/project/snippet/public_access_spec.rb +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe "Public Project Snippets Access", feature: true do include AccessMatchers - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public) } let(:owner) { project.owner } let(:master) { create(:user) } @@ -49,45 +49,91 @@ describe "Public Project Snippets Access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/snippets/:id for a public snippet" do - subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } + describe "GET /:project_path/snippets/:id" do + context "for a public snippet" do + subject { namespace_project_snippet_path(project.namespace, project, public_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_allowed_for :external } - it { is_expected.to be_allowed_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + context "for an internal snippet" do + subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + context "for a private snippet" do + subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end end - describe "GET /:project_path/snippets/:id for an internal snippet" do - subject { namespace_project_snippet_path(project.namespace, project, internal_snippet) } + describe "GET /:project_path/snippets/:id/raw" do + context "for a public snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, public_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end - describe "GET /:project_path/snippets/:id for a private snippet" do - subject { namespace_project_snippet_path(project.namespace, project, private_snippet) } + context "for an internal snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, internal_snippet) } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for owner } - it { is_expected.to be_allowed_for master } - it { is_expected.to be_allowed_for developer } - it { is_expected.to be_allowed_for reporter } - it { is_expected.to be_allowed_for guest } - it { is_expected.to be_denied_for :user } - it { is_expected.to be_denied_for :external } - it { is_expected.to be_denied_for :visitor } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + + context "for a private snippet" do + subject { raw_namespace_project_snippet_path(project.namespace, project, private_snippet) } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for owner } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for developer } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end end end diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb new file mode 100644 index 00000000000..34300ccb940 --- /dev/null +++ b/spec/features/snippets/public_snippets_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +feature 'Public Snippets', feature: true do + scenario 'Unauthenticated user should see public snippets' do + public_snippet = create(:personal_snippet, :public) + + visit snippet_path(public_snippet) + + expect(page).to have_content(public_snippet.content) + end + + scenario 'Unauthenticated user should see raw public snippets' do + public_snippet = create(:personal_snippet, :public) + + visit raw_snippet_path(public_snippet) + + expect(page).to have_content(public_snippet.content) + end +end diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_incoming_email_token.eml similarity index 100% rename from spec/fixtures/emails/wrong_authentication_token.eml rename to spec/fixtures/emails/wrong_incoming_email_token.eml diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 73f5470cf35..c706e418d26 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -218,42 +218,24 @@ describe ApplicationHelper do end it 'includes a default js-timeago class' do - expect(element.attr('class')).to eq 'js-timeago js-timeago-pending' + expect(element.attr('class')).to eq 'js-timeago' end it 'accepts a custom html_class' do expect(element(html_class: 'custom_class').attr('class')). - to eq 'js-timeago custom_class js-timeago-pending' + to eq 'js-timeago custom_class' end it 'accepts a custom tooltip placement' do expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom' end - it 're-initializes timeago Javascript' do - el = element.next_element - - expect(el.name).to eq 'script' - expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()" - end - - it 'allows the script tag to be excluded' do - expect(element(skip_js: true)).not_to include 'script' - end - it 'converts to Time' do expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error end - it 'add class for the short format and includes inline script' do + it 'add class for the short format' do timeago_element = element(short_format: 'short') - expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending' - script_element = timeago_element.next_element - expect(script_element.name).to eq 'script' - end - - it 'add class for the short format and does not include inline script' do - timeago_element = element(short_format: 'short', skip_js: true) expect(timeago_element.attr('class')).to eq 'js-short-timeago' expect(timeago_element.next_element).to eq nil end diff --git a/spec/helpers/components_helper_spec.rb b/spec/helpers/components_helper_spec.rb new file mode 100644 index 00000000000..94a59193be8 --- /dev/null +++ b/spec/helpers/components_helper_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe ComponentsHelper do + describe '#gitlab_workhorse_version' do + context 'without a Gitlab-Workhorse header' do + it 'shows the version from Gitlab::Workhorse.version' do + expect(helper.gitlab_workhorse_version).to eq Gitlab::Workhorse.version + end + end + + context 'with a Gitlab-Workhorse header' do + before do + helper.request.headers['Gitlab-Workhorse'] = '42.42.0-rc3' + end + + it 'shows the actual GitLab Workhorse version currently in use' do + expect(helper.gitlab_workhorse_version).to eq '42.42.0' + end + end + end +end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 9c7c79f57c6..837e7afa7e8 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -61,7 +61,7 @@ describe DiffHelper do describe '#diff_line_content' do it 'returns non breaking space when line is empty' do - expect(diff_line_content(nil)).to eq('  ') + expect(diff_line_content(nil)).to eq(' ') end it 'returns the line itself' do diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc new file mode 100644 index 00000000000..90388929612 --- /dev/null +++ b/spec/javascripts/.eslintrc @@ -0,0 +1,11 @@ +{ + "plugins": ["jasmine"], + "env": { + "jasmine": true + }, + "extends": "plugin:jasmine/recommended", + "rules": { + "prefer-arrow-callback": 0, + "func-names": 0 + } +} diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 6208c2386b0..b84dfc8197b 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -13,8 +13,9 @@ //= require boards/stores/boards_store //= require ./mock_data -(() => { +describe('Store', () => { beforeEach(() => { + Vue.http.interceptors.push(boardsMockInterceptor); gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.issueBoards.BoardsStore.create(); @@ -24,145 +25,147 @@ }); }); - describe('Store', () => { - it('starts with a blank state', () => { + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + + it('starts with a blank state', () => { + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); + }); + + describe('lists', () => { + it('creates new list without persisting to DB', () => { + gl.issueBoards.BoardsStore.addList(listObj); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + }); + + it('finds list by ID', () => { + gl.issueBoards.BoardsStore.addList(listObj); + const list = gl.issueBoards.BoardsStore.findList('id', 1); + + expect(list.id).toBe(1); + }); + + it('finds list by type', () => { + gl.issueBoards.BoardsStore.addList(listObj); + const list = gl.issueBoards.BoardsStore.findList('type', 'label'); + + expect(list).toBeDefined(); + }); + + it('finds list limited by type', () => { + gl.issueBoards.BoardsStore.addList({ + id: 1, + position: 0, + title: 'Test', + list_type: 'backlog' + }); + const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog'); + + expect(list).toBeDefined(); + }); + + it('gets issue when new list added', (done) => { + gl.issueBoards.BoardsStore.addList(listObj); + const list = gl.issueBoards.BoardsStore.findList('id', 1); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + + setTimeout(() => { + expect(list.issues.length).toBe(1); + expect(list.issues[0].id).toBe(1); + done(); + }, 0); + }); + + it('persists new list', (done) => { + gl.issueBoards.BoardsStore.new({ + title: 'Test', + type: 'label', + label: { + id: 1, + title: 'Testing', + color: 'red', + description: 'testing;' + } + }); + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + + setTimeout(() => { + const list = gl.issueBoards.BoardsStore.findList('id', 1); + expect(list).toBeDefined(); + expect(list.id).toBe(1); + expect(list.position).toBe(0); + done(); + }, 0); + }); + + it('check for blank state adding', () => { + expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); + }); + + it('check for blank state not adding', () => { + gl.issueBoards.BoardsStore.addList(listObj); + expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); + }); + + it('check for blank state adding when backlog & done list exist', () => { + gl.issueBoards.BoardsStore.addList({ + list_type: 'backlog' + }); + gl.issueBoards.BoardsStore.addList({ + list_type: 'done' + }); + + expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); + }); + + it('adds the blank state', () => { + gl.issueBoards.BoardsStore.addBlankState(); + + const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank'); + expect(list).toBeDefined(); + }); + + it('removes list from state', () => { + gl.issueBoards.BoardsStore.addList(listObj); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); + + gl.issueBoards.BoardsStore.removeList(1, 'label'); + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); }); - describe('lists', () => { - it('creates new list without persisting to DB', () => { - gl.issueBoards.BoardsStore.addList(listObj); + it('moves the position of lists', () => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj), + listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - }); + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - it('finds list by ID', () => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']); - expect(list.id).toBe(1); - }); + expect(listOne.position).toBe(1); + }); - it('finds list by type', () => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('type', 'label'); + it('moves an issue from one list to another', (done) => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj), + listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); - expect(list).toBeDefined(); - }); + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - it('finds list limited by type', () => { - gl.issueBoards.BoardsStore.addList({ - id: 1, - position: 0, - title: 'Test', - list_type: 'backlog' - }); - const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog'); + setTimeout(() => { + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); - expect(list).toBeDefined(); - }); + gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1)); - it('gets issue when new list added', (done) => { - gl.issueBoards.BoardsStore.addList(listObj); - const list = gl.issueBoards.BoardsStore.findList('id', 1); + expect(listOne.issues.length).toBe(0); + expect(listTwo.issues.length).toBe(1); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - - setTimeout(() => { - expect(list.issues.length).toBe(1); - expect(list.issues[0].id).toBe(1); - done(); - }, 0); - }); - - it('persists new list', (done) => { - gl.issueBoards.BoardsStore.new({ - title: 'Test', - type: 'label', - label: { - id: 1, - title: 'Testing', - color: 'red', - description: 'testing;' - } - }); - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - - setTimeout(() => { - const list = gl.issueBoards.BoardsStore.findList('id', 1); - expect(list).toBeDefined(); - expect(list.id).toBe(1); - expect(list.position).toBe(0); - done(); - }, 0); - }); - - it('check for blank state adding', () => { - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); - }); - - it('check for blank state not adding', () => { - gl.issueBoards.BoardsStore.addList(listObj); - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); - }); - - it('check for blank state adding when backlog & done list exist', () => { - gl.issueBoards.BoardsStore.addList({ - list_type: 'backlog' - }); - gl.issueBoards.BoardsStore.addList({ - list_type: 'done' - }); - - expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true); - }); - - it('adds the blank state', () => { - gl.issueBoards.BoardsStore.addBlankState(); - - const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank'); - expect(list).toBeDefined(); - }); - - it('removes list from state', () => { - gl.issueBoards.BoardsStore.addList(listObj); - - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1); - - gl.issueBoards.BoardsStore.removeList(1, 'label'); - - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0); - }); - - it('moves the position of lists', () => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj), - listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); - - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - - gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']); - - expect(listOne.position).toBe(1); - }); - - it('moves an issue from one list to another', (done) => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj), - listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); - - expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); - - setTimeout(() => { - expect(listOne.issues.length).toBe(1); - expect(listTwo.issues.length).toBe(1); - - gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1)); - - expect(listOne.issues.length).toBe(0); - expect(listTwo.issues.length).toBe(1); - - done(); - }, 0); - }); + done(); + }, 0); }); }); -})(); +}); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 1a0427fdd90..dfbcbe3a7c1 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -17,12 +17,17 @@ describe('List model', () => { let list; beforeEach(() => { + Vue.http.interceptors.push(boardsMockInterceptor); gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.issueBoards.BoardsStore.create(); list = new List(listObj); }); + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor); + }); + it('gets issues when created', (done) => { setTimeout(() => { expect(list.issues.length).toBe(1); diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 80d05e8a1a3..fcb3d8f17d8 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -48,10 +48,10 @@ const BoardsMockData = { } }; -Vue.http.interceptors.push((request, next) => { +const boardsMockInterceptor = (request, next) => { const body = BoardsMockData[request.method][request.url]; next(request.respondWith(JSON.stringify(body), { status: 200 })); -}); +}; diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 new file mode 100644 index 00000000000..370944b6a8c --- /dev/null +++ b/spec/javascripts/build_spec.js.es6 @@ -0,0 +1,175 @@ +/* global Build */ +/* eslint-disable no-new */ +//= require build +//= require breakpoints +//= require jquery.nicescroll +//= require turbolinks + +(() => { + describe('Build', () => { + fixture.preload('build.html'); + + beforeEach(function () { + fixture.load('build.html'); + spyOn($, 'ajax'); + }); + + describe('constructor', () => { + beforeEach(function () { + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + describe('setup', function () { + beforeEach(function () { + this.build = new Build(); + }); + + it('copies build options', function () { + expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2'); + expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json'); + expect(this.build.buildStatus).toBe('passed'); + expect(this.build.buildStage).toBe('test'); + expect(this.build.state).toBe('buildstate'); + }); + + it('only shows the jobs matching the current stage', function () { + expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false); + expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true); + expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); + }); + + it('selects the current stage in the build dropdown menu', function () { + expect($('.stage-selection').text()).toBe('test'); + }); + + it('updates the jobs when the build dropdown changes', function () { + $('.stage-item:contains("build")').click(); + + expect($('.stage-selection').text()).toBe('build'); + expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true); + expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); + expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); + }); + }); + + describe('initial build trace', function () { + beforeEach(function () { + new Build(); + }); + + it('displays the initial build trace', function () { + expect($.ajax.calls.count()).toBe(1); + const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0); + expect(url).toBe('http://example.com/root/test-build/builds/2.json'); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { trace_html: 'Example', status: 'running' }); + + expect($('#build-trace .js-build-output').text()).toMatch(/Example/); + }); + + it('removes the spinner', function () { + const [{ success, context }] = $.ajax.calls.argsFor(0); + success.call(context, { trace_html: 'Example', status: 'success' }); + + expect($('.js-build-refresh').length).toBe(0); + }); + }); + + describe('running build', function () { + beforeEach(function () { + $('.js-build-options').data('buildStatus', 'running'); + this.build = new Build(); + spyOn(this.build, 'location') + .and.returnValue('http://example.com/root/test-build/builds/2'); + }); + + it('updates the build trace on an interval', function () { + jasmine.clock().tick(4001); + + expect($.ajax.calls.count()).toBe(2); + let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); + expect(url).toBe( + 'http://example.com/root/test-build/builds/2/trace.json?state=buildstate' + ); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { + html: 'Update', + status: 'running', + state: 'newstate', + append: true, + }); + + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + expect(this.build.state).toBe('newstate'); + + jasmine.clock().tick(4001); + + expect($.ajax.calls.count()).toBe(3); + [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2); + expect(url).toBe( + 'http://example.com/root/test-build/builds/2/trace.json?state=newstate' + ); + expect(dataType).toBe('json'); + expect(success).toEqual(jasmine.any(Function)); + + success.call(context, { + html: 'More', + status: 'running', + state: 'finalstate', + append: true, + }); + + expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); + expect(this.build.state).toBe('finalstate'); + }); + + it('replaces the entire build trace', function () { + jasmine.clock().tick(4001); + let [{ success, context }] = $.ajax.calls.argsFor(1); + success.call(context, { + html: 'Update', + status: 'running', + append: true, + }); + + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + + jasmine.clock().tick(4001); + [{ success, context }] = $.ajax.calls.argsFor(2); + success.call(context, { + html: 'Different', + status: 'running', + append: false, + }); + + expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); + expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + }); + + it('reloads the page when the build is done', function () { + spyOn(Turbolinks, 'visit'); + + jasmine.clock().tick(4001); + const [{ success, context }] = $.ajax.calls.argsFor(1); + success.call(context, { + html: 'Final', + status: 'passed', + append: true, + }); + + expect(Turbolinks.visit).toHaveBeenCalledWith( + 'http://example.com/root/test-build/builds/2' + ); + }); + }); + }); + }); +})(); diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 5d817802602..9b2845af608 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -92,7 +92,6 @@ it('is unresolved with 2 notes', () => { const discussion = CommentsStore.state['a']; createDiscussion(2, false); - console.log(discussion.isResolved()); expect(discussion.isResolved()).toBe(false); }); diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml new file mode 100644 index 00000000000..a2bc81c6be7 --- /dev/null +++ b/spec/javascripts/fixtures/build.html.haml @@ -0,0 +1,57 @@ +.build-page + .prepend-top-default + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + #js-build-scroll.scroll-controls + %a.btn{href: '#build-trace'} + %i.fa.fa-angle-up + %a.btn{href: '#down-build-trace'} + %i.fa.fa-angle-down + %pre.build-trace#build-trace + %code.bash.js-build-output + %i.fa.fa-refresh.fa-spin.js-build-refresh + +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar + .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default + Build + %strong #1 + %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } + %i.fa.fa-angle-double-right + .blocks-container + .dropdown.build-dropdown + .title Stage + %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} + %span.stage-selection More + %i.fa.fa-caret-down + %ul.dropdown-menu + %li + %a.stage-item build + %li + %a.stage-item test + %li + %a.stage-item deploy + .builds-container + .build-job{data: {stage: 'build'}} + %a{href: 'http://example.com/root/test-build/builds/1'} + %i.fa.fa-check + %i.fa.fa-check-circle-o + %span + Setup + .build-job{data: {stage: 'test'}} + %a{href: 'http://example.com/root/test-build/builds/2'} + %i.fa.fa-check + %i.fa.fa-check-circle-o + %span + Tests + .build-job{data: {stage: 'deploy'}} + %a{href: 'http://example.com/root/test-build/builds/3'} + %i.fa.fa-check + %i.fa.fa-check-circle-o + %span + Deploy + +.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2', + build_url: 'http://example.com/root/test-build/builds/2.json', + build_status: 'passed', + build_stage: 'test', + state1: 'buildstate' }} diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 49dfeab61d8..91f19aca719 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,6 @@ /* eslint-disable */ /*= require merge_request_widget */ -/*= require jquery.timeago.js */ +/*= require lib/utils/timeago.js */ (function() { describe('MergeRequestWidget', function() { diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index dca7f997570..a6d2ea11fcc 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do expect(doc.at_css('a')['href']).to eq link end + it 'autolinks rdar' do + link = 'rdar://localhost.com/blah' + doc = filter("See #{link}") + + expect(doc.at_css('a').text).to eq link + expect(doc.at_css('a')['href']).to eq link + end + + it 'does not autolink javascript' do + link = 'javascript://alert(document.cookie);' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to be_nil + end + + it 'does not autolink bad URLs' do + link = 'foo://23423:::asdf' + doc = filter("See #{link}") + + expect(doc.to_s).to eq("See #{link}") + end + it 'does not include trailing punctuation' do doc = filter("See #{link}.") expect(doc.at_css('a').text).to eq link diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index f181125156b..0140a91c7ba 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do and_return(parser_class) end - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) + context 'valid projects' do + before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) } - link = reference_link(project: project.id, reference_type: 'test') - doc = filter(link, current_user: user) + it 'allows permitted Project references' do + user = create(:user) + project = create(:empty_project) + project.team << [user, :master] - expect(doc.css('a').length).to eq 0 + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end end - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - project.team << [user, :master] + context 'invalid projects' do + before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) } - link = reference_link(project: project.id, reference_type: 'test') - doc = filter(link, current_user: user) + it 'removes unpermitted references' do + user = create(:user) + project = create(:empty_project) - expect(doc.css('a').length).to eq 1 - end + link = reference_link(project: project.id, reference_type: 'test') + doc = filter(link, current_user: user) - it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_type: 'test') + expect(doc.css('a').length).to eq 0 + end - expect { filter(link) }.not_to raise_error + it 'handles invalid references' do + link = reference_link(project: 12345, reference_type: 'test') + + expect { filter(link) }.not_to raise_error + end end end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index 9095d2b1345..aa127f0179d 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do let(:link) { empty_html_link } context 'when the link has a data-project attribute' do - it 'returns the nodes if the attribute value equals the current project ID' do + it 'checks if user can read the resource' do link['data-project'] = project.id.to_s - expect(Ability).not_to receive(:allowed?) - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end + expect(subject).to receive(:can_read_reference?).with(user, project) - it 'returns the nodes if the user can read the project' do - other_project = create(:empty_project, :public) - - link['data-project'] = other_project.id.to_s - - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(true) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) - end - - it 'returns an empty Array when the attribute value is empty' do - link['data-project'] = '' - - expect(subject.nodes_visible_to_user(user, [link])).to eq([]) - end - - it 'returns an empty Array when the user can not read the project' do - other_project = create(:empty_project, :public) - - link['data-project'] = other_project.id.to_s - - expect(Ability).to receive(:allowed?). - with(user, :read_project, other_project). - and_return(false) - - expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + subject.nodes_visible_to_user(user, [link]) end end diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 0b76d29fce0..412ffa77c36 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-commit'] = 123 } + + it_behaves_like "referenced feature visibility", "repository" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index ba982f38542..96e55b0997a 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-commit-range'] = '123..456' } + + it_behaves_like "referenced feature visibility", "repository" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index a6ef8394fe7..50a5d1a19ba 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-external-issue'] = 123 } + + it_behaves_like "referenced feature visibility", "issues" + end + end + describe '#referenced_by' do context 'when the link has a data-project attribute' do before do diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 85cfe728b6a..6873b7b85f9 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -4,10 +4,10 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do include ReferenceParserHelpers let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } - subject { described_class.new(project, user) } - let(:link) { empty_html_link } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:link) { empty_html_link } + subject { described_class.new(project, user) } describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do @@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do link['data-issue'] = issue.id.to_s end + it_behaves_like "referenced feature visibility", "issues" + it 'returns the nodes when the user can read the issue' do expect(Ability).to receive(:issues_readable_by_user). with([issue], user). diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb index 77fda47f0e7..8c540d35ddd 100644 --- a/spec/lib/banzai/reference_parser/label_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-label'] = label.id.to_s } + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-label attribute' do context 'using an existing label ID' do diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb index cf89ad598ea..cb69ca16800 100644 --- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb @@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do subject { described_class.new(merge_request.target_project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + let(:project) { merge_request.target_project } + + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + link['data-merge-request'] = merge_request.id.to_s + end + + it_behaves_like "referenced feature visibility", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-merge-request attribute' do context 'using an existing merge request ID' do diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb index 6aa45a22cc4..2d4d589ae34 100644 --- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-milestone'] = milestone.id.to_s } + + it_behaves_like "referenced feature visibility", "issues", "merge_requests" + end + end + describe '#referenced_by' do describe 'when the link has a data-milestone attribute' do context 'using an existing milestone ID' do diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb index 59127b7c5d1..d217a775802 100644 --- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do subject { described_class.new(project, user) } let(:link) { empty_html_link } + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before { link['data-snippet'] = snippet.id.to_s } + + it_behaves_like "referenced feature visibility", "snippets" + end + end + describe '#referenced_by' do describe 'when the link has a data-snippet attribute' do context 'using an existing snippet ID' do diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 4e7f82a6e09..fafc2cec546 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -103,6 +103,8 @@ describe Banzai::ReferenceParser::UserParser, lib: true do it 'returns the nodes if the attribute value equals the current project ID' do link['data-project'] = project.id.to_s + # Ensure that we dont call for Ability.allowed? + # When project_id in the node is equal to current project ID expect(Ability).not_to receive(:allowed?) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) diff --git a/spec/lib/constraints/constrainer_helper_spec.rb b/spec/lib/constraints/constrainer_helper_spec.rb new file mode 100644 index 00000000000..27c8d72aefc --- /dev/null +++ b/spec/lib/constraints/constrainer_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe ConstrainerHelper, lib: true do + include ConstrainerHelper + + describe '#extract_resource_path' do + it { expect(extract_resource_path('/gitlab/')).to eq('gitlab') } + it { expect(extract_resource_path('///gitlab//')).to eq('gitlab') } + it { expect(extract_resource_path('/gitlab.atom')).to eq('gitlab') } + + context 'relative url' do + before do + allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } + end + + it { expect(extract_resource_path('/gitlab/foo')).to eq('foo') } + it { expect(extract_resource_path('/foo/bar')).to eq('foo/bar') } + end + end +end diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb index f0b75a664f2..42299b17c2b 100644 --- a/spec/lib/constraints/group_url_constrainer_spec.rb +++ b/spec/lib/constraints/group_url_constrainer_spec.rb @@ -1,10 +1,19 @@ require 'spec_helper' describe GroupUrlConstrainer, lib: true do - let!(:username) { create(:group, path: 'gitlab-org') } + let!(:group) { create(:group, path: 'gitlab') } - describe '#find_resource' do - it { expect(!!subject.find_resource('gitlab-org')).to be_truthy } - it { expect(!!subject.find_resource('gitlab-com')).to be_falsey } + describe '#matches?' do + context 'root group' do + it { expect(subject.matches?(request '/gitlab')).to be_truthy } + it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy } + it { expect(subject.matches?(request '/gitlab/edit')).to be_falsey } + it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey } + it { expect(subject.matches?(request '/.gitlab')).to be_falsey } + end + end + + def request(path) + double(:request, path: path) end end diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb deleted file mode 100644 index 7814711fe27..00000000000 --- a/spec/lib/constraints/namespace_url_constrainer_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'spec_helper' - -describe NamespaceUrlConstrainer, lib: true do - let!(:group) { create(:group, path: 'gitlab') } - - describe '#matches?' do - context 'existing namespace' do - it { expect(subject.matches?(request '/gitlab')).to be_truthy } - it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy } - it { expect(subject.matches?(request '/gitlab/')).to be_truthy } - it { expect(subject.matches?(request '//gitlab/')).to be_truthy } - end - - context 'non-existing namespace' do - it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey } - it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey } - it { expect(subject.matches?(request '/g/gitlab')).to be_falsey } - it { expect(subject.matches?(request '/.gitlab')).to be_falsey } - end - - context 'relative url' do - before do - allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } - end - - it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy } - it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey } - it { expect(subject.matches?(request '/gitlab/')).to be_falsey } - end - end - - def request(path) - OpenStruct.new(path: path) - end -end diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb index 4b26692672f..b3f8530c609 100644 --- a/spec/lib/constraints/user_url_constrainer_spec.rb +++ b/spec/lib/constraints/user_url_constrainer_spec.rb @@ -3,8 +3,14 @@ require 'spec_helper' describe UserUrlConstrainer, lib: true do let!(:username) { create(:user, username: 'dz') } - describe '#find_resource' do - it { expect(!!subject.find_resource('dz')).to be_truthy } - it { expect(!!subject.find_resource('john')).to be_falsey } + describe '#matches?' do + it { expect(subject.matches?(request '/dz')).to be_truthy } + it { expect(subject.matches?(request '/dz.atom')).to be_truthy } + it { expect(subject.matches?(request '/dz/projects')).to be_falsey } + it { expect(subject.matches?(request '/gitlab')).to be_falsey } + end + + def request(path) + double(:request, path: path) end end diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index f826d0d1b04..4b08a02ec73 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -14,7 +14,6 @@ describe Gitlab::Shell, lib: true do it { is_expected.to respond_to :add_repository } it { is_expected.to respond_to :remove_repository } it { is_expected.to respond_to :fork_repository } - it { is_expected.to respond_to :gc } it { is_expected.to respond_to :add_namespace } it { is_expected.to respond_to :rm_namespace } it { is_expected.to respond_to :mv_namespace } diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index de3f64249a2..1bbaca0739a 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do context 'with an external issue tracker reference' do it 'extracts the referenced issue' do jira_project = create(:jira_project, name: 'JIRA_EXT1') + jira_project.team << [jira_project.creator, :master] jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) - closing_issue_extractor = described_class.new jira_project + closing_issue_extractor = described_class.new(jira_project, jira_project.creator) message = "Resolve #{jira_issue.to_reference}" expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue]) diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb new file mode 100644 index 00000000000..01b2a55b63c --- /dev/null +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Gitlab::ContributionsCalendar do + let(:contributor) { create(:user) } + let(:user) { create(:user) } + + let(:private_project) do + create(:empty_project, :private) do |project| + create(:project_member, user: contributor, project: project) + end + end + + let(:public_project) do + create(:empty_project, :public) do |project| + create(:project_member, user: contributor, project: project) + end + end + + let(:feature_project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project| + create(:project_member, user: contributor, project: project).project + end + end + + let(:today) { Time.now.to_date } + let(:last_week) { today - 7.days } + let(:last_year) { today - 1.year } + + before do + travel_to today + end + + after do + travel_back + end + + def calendar(current_user = nil) + described_class.new(contributor, current_user) + end + + def create_event(project, day) + @targets ||= {} + @targets[project] ||= create(:issue, project: project, author: contributor) + + Event.create!( + project: project, + action: Event::CREATED, + target: @targets[project], + author: contributor, + created_at: day, + ) + end + + describe '#activity_dates' do + it "returns a hash of date => count" do + create_event(public_project, last_week) + create_event(public_project, last_week) + create_event(public_project, today) + + expect(calendar.activity_dates).to eq(last_week => 2, today => 1) + end + + it "only shows private events to authorized users" do + create_event(private_project, today) + create_event(feature_project, today) + + expect(calendar.activity_dates[today]).to eq(0) + expect(calendar(user).activity_dates[today]).to eq(0) + expect(calendar(contributor).activity_dates[today]).to eq(2) + end + end + + describe '#events_by_date' do + it "returns all events for a given date" do + e1 = create_event(public_project, today) + e2 = create_event(public_project, today) + create_event(public_project, last_week) + + expect(calendar.events_by_date(today)).to contain_exactly(e1, e2) + end + + it "only shows private events to authorized users" do + e1 = create_event(public_project, today) + e2 = create_event(private_project, today) + e3 = create_event(feature_project, today) + create_event(public_project, last_week) + + expect(calendar.events_by_date(today)).to contain_exactly(e1) + expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) + end + end + + describe '#starting_year' do + it "should be the start of last year" do + expect(calendar.starting_year).to eq(last_year.year) + end + end + + describe '#starting_month' do + it "should be the start of this month" do + expect(calendar.starting_month).to eq(today.month) + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index a5cc7b02936..cb3651e3845 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require_relative '../email_shared_blocks' -xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do +describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do include_context :email_shared_context it_behaves_like :email_shared_examples @@ -18,7 +18,7 @@ xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do create( :user, email: 'jake@adventuretime.ooo', - authentication_token: 'auth_token' + incoming_email_token: 'auth_token' ) end @@ -60,8 +60,8 @@ xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do end end - context "when we can't find the authentication_token" do - let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") } + context "when we can't find the incoming_email_token" do + let(:email_raw) { fixture_file("emails/wrong_incoming_email_token.eml") } it "raises an UserNotFoundError" do expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index 6b3bd08b978..a366d68a146 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -5,32 +5,47 @@ describe Gitlab::ExclusiveLease, type: :redis do describe '#try_obtain' do it 'cannot obtain twice before the lease has expired' do - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to eq(true) + lease = described_class.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to be_present expect(lease.try_obtain).to eq(false) end it 'can obtain after the lease has expired' do timeout = 1 - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) + lease = described_class.new(unique_key, timeout: timeout) lease.try_obtain # start the lease sleep(2 * timeout) # lease should have expired now - expect(lease.try_obtain).to eq(true) + expect(lease.try_obtain).to be_present end end describe '#exists?' do it 'returns true for an existing lease' do - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + lease = described_class.new(unique_key, timeout: 3600) lease.try_obtain expect(lease.exists?).to eq(true) end it 'returns false for a lease that does not exist' do - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + lease = described_class.new(unique_key, timeout: 3600) expect(lease.exists?).to eq(false) end end + + describe '.cancel' do + it 'can cancel a lease' do + uuid = new_lease(unique_key) + expect(uuid).to be_present + expect(new_lease(unique_key)).to eq(false) + + described_class.cancel(unique_key, uuid) + expect(new_lease(unique_key)).to be_present + end + + def new_lease(key) + described_class.new(key, timeout: 3600).try_obtain + end + end end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index f045463c1cb..6b3dfebd85d 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Gfm::ReferenceRewriter do let(:new_project) { create(:project, name: 'new') } let(:user) { create(:user) } - before { old_project.team << [user, :guest] } + before { old_project.team << [user, :reporter] } describe '#rewrite' do subject do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 62aa212f1f6..f1d0a190002 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -66,6 +66,7 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } + it { expect(subject.message).to match(/You are not allowed to download code/) } end end @@ -77,6 +78,7 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } + it { expect(subject.message).to match(/Your account has been blocked/) } end end @@ -84,6 +86,29 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do it { expect(subject.allowed?).to be_falsey } end + + context 'when project is public' do + let(:public_project) { create(:project, :public) } + let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) } + subject { guest_access.check('git-upload-pack', '_any') } + + context 'when repository is enabled' do + it 'give access to download code' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) + + expect(subject.allowed?).to be_truthy + end + end + + context 'when repository is disabled' do + it 'does not give access to download code' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + + expect(subject.allowed?).to be_falsey + expect(subject.message).to match(/You are not allowed to download code/) + end + end + end end describe 'deploy key permissions' do diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 576cda595bb..576aa5c366f 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki, lib: true do project.team << [user, :developer] end - subject { access.push_access_check(changes) } + subject { access.check('git-receive-pack', changes) } it { expect(subject.allowed?).to be_truthy } end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 835853a83a4..f5ebe703083 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -1,20 +1,51 @@ require 'spec_helper' describe Gitlab::LDAP::Config, lib: true do - let(:config) { Gitlab::LDAP::Config.new provider } - let(:provider) { 'ldapmain' } + include LdapHelpers + + let(:config) { Gitlab::LDAP::Config.new('ldapmain') } describe '#initalize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end - it "works" do + it 'works' do expect(config).to be_a described_class end - it "raises an error if a unknow provider is used" do + it 'raises an error if a unknown provider is used' do expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError) end end + + describe '#has_auth?' do + it 'is true when password is set' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => 'super_secret' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is true when bind_dn is set and password is empty' do + stub_ldap_config( + options: { + 'bind_dn' => 'uid=admin,dc=example,dc=com', + 'password' => '' + } + ) + + expect(config.has_auth?).to be_truthy + end + + it 'is false when password and bind_dn are not set' do + stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil }) + + expect(config.has_auth?).to be_falsey + end + end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7b4ccc83915..bf0ab9635fd 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor, lib: true do let(:project) { create(:project) } + before { project.team << [project.creator, :developer] } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } it 'accesses valid user objects' do @@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do end it 'accesses valid issue objects' do - project.team << [project.creator, :developer] @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 2b76e056f3c..b950fcdd81a 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -98,6 +98,24 @@ describe ApplicationSetting, models: true do end end end + + context 'housekeeping settings' do + it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) } + + it 'wants the full repack period to be longer than the incremental repack period' do + subject.housekeeping_incremental_repack_period = 2 + subject.housekeeping_full_repack_period = 1 + + expect(subject).not_to be_valid + end + + it 'wants the gc period to be longer than the full repack period' do + subject.housekeeping_full_repack_period = 2 + subject.housekeeping_gc_period = 1 + + expect(subject).not_to be_valid + end + end end context 'restricted signup domains' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5eb14dc6bd2..71b7628ef10 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -524,4 +524,78 @@ describe Ci::Pipeline, models: true do expect(pipeline.merge_requests).to be_empty end end + + describe 'notifications when pipeline success or failed' do + let(:project) { create(:project) } + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('master').sha, + user: create(:user)) + end + + before do + reset_delivered_emails! + + project.team << [pipeline.user, Gitlab::Access::DEVELOPER] + + perform_enqueued_jobs do + pipeline.enqueue + pipeline.run + end + end + + shared_examples 'sending a notification' do + it 'sends an email' do + should_only_email(pipeline.user, kind: :bcc) + end + end + + shared_examples 'not sending any notification' do + it 'does not send any email' do + should_not_email_anyone + end + end + + context 'with success pipeline' do + before do + perform_enqueued_jobs do + pipeline.succeed + end + end + + it_behaves_like 'sending a notification' + end + + context 'with failed pipeline' do + before do + perform_enqueued_jobs do + pipeline.drop + end + end + + it_behaves_like 'sending a notification' + end + + context 'with skipped pipeline' do + before do + perform_enqueued_jobs do + pipeline.skip + end + end + + it_behaves_like 'not sending any notification' + end + + context 'with cancelled pipeline' do + before do + perform_enqueued_jobs do + pipeline.cancel + end + end + + it_behaves_like 'not sending any notification' + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 51be3f36135..e3bb3482d67 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -205,12 +205,53 @@ eos end end - describe '#ci_commits' do - # TODO: kamil - end - describe '#status' do - # TODO: kamil + context 'without arguments for compound status' do + shared_examples 'giving the status from pipeline' do + it do + expect(commit.status).to eq(Ci::Pipeline.status) + end + end + + context 'with pipelines' do + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, sha: commit.sha) + end + + it_behaves_like 'giving the status from pipeline' + end + + context 'without pipelines' do + it_behaves_like 'giving the status from pipeline' + end + end + + context 'when a particular ref is specified' do + let!(:pipeline_from_master) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + ref: 'master', + status: 'failed') + end + + let!(:pipeline_from_fix) do + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + ref: 'fix', + status: 'success') + end + + it 'gives pipelines from a particular branch' do + expect(commit.status('master')).to eq(pipeline_from_master.status) + expect(commit.status('fix')).to eq(pipeline_from_fix.status) + end + + it 'gives compound status if ref is nil' do + expect(commit.status(nil)).to eq(commit.status) + end + end end describe '#participants' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a59d30687f6..6e987967ca5 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -97,6 +97,11 @@ describe Issue, "Issuable" do end end + describe '.to_ability_name' do + it { expect(Issue.to_ability_name).to eq("issue") } + it { expect(MergeRequest.to_ability_name).to eq("merge_request") } + end + describe "#today?" do it "returns true when created today" do # Avoid timezone differences and just return exactly what we want @@ -341,4 +346,25 @@ describe Issue, "Issuable" do expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2]) end end + + describe '#assignee_or_author?' do + let(:user) { build(:user, id: 1) } + let(:issue) { build(:issue) } + + it 'returns true for a user that is assigned to an issue' do + issue.assignee = user + + expect(issue.assignee_or_author?(user)).to eq(true) + end + + it 'returns true for a user that is the author of an issue' do + issue.author = user + + expect(issue.assignee_or_author?(user)).to eq(true) + end + + it 'returns false for a user that is not the assignee or author' do + expect(issue.assignee_or_author?(user)).to eq(false) + end + end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index aca49be2942..29a3af68a9b 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -27,13 +27,14 @@ describe Event, models: true do end describe "Push event" do - let(:project) { create(:project) } + let(:project) { create(:project, :private) } let(:user) { project.owner } let(:event) { create_event(project, user) } it do expect(event.push?).to be_truthy - expect(event.visible_to_user?).to be_truthy + expect(event.visible_to_user?(user)).to be_truthy + expect(event.visible_to_user?(nil)).to be_falsey expect(event.tag?).to be_falsey expect(event.branch_name).to eq("master") expect(event.author).to eq(user) diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index ebba6e14578..2debe1289a3 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe ExternalIssue, models: true do - let(:project) { double('project', to_reference: 'namespace1/project1') } + let(:project) { double('project', id: 1, to_reference: 'namespace1/project1') } let(:issue) { described_class.new('EXT-1234', project) } describe 'modules' do @@ -36,4 +36,10 @@ describe ExternalIssue, models: true do end end end + + describe '#project_id' do + it 'returns the ID of the project' do + expect(issue.project_id).to eq(project.id) + end + end end diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb new file mode 100644 index 00000000000..d79f929f7a1 --- /dev/null +++ b/spec/models/guest_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Guest, lib: true do + let(:public_project) { create(:project, :public) } + let(:private_project) { create(:project, :private) } + let(:internal_project) { create(:project, :internal) } + + describe '.can_pull?' do + context 'when project is private' do + it 'does not allow to pull the repo' do + expect(Guest.can?(:download_code, private_project)).to eq(false) + end + end + + context 'when project is internal' do + it 'does not allow to pull the repo' do + expect(Guest.can?(:download_code, internal_project)).to eq(false) + end + end + + context 'when project is public' do + context 'when repository is disabled' do + it 'does not allow to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + + expect(Guest.can?(:download_code, public_project)).to eq(false) + end + end + + context 'when repository is accessible only by team members' do + it 'does not allow to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE) + + expect(Guest.can?(:download_code, public_project)).to eq(false) + end + end + + context 'when repository is enabled' do + it 'allows to pull the repo' do + public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) + + expect(Guest.can?(:download_code, public_project)).to eq(true) + end + end + end + end +end diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb new file mode 100644 index 00000000000..d742c814680 --- /dev/null +++ b/spec/models/issue_collection_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe IssueCollection do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issue1) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project) } + let(:collection) { described_class.new([issue1, issue2]) } + + describe '#collection' do + it 'returns the issues in the same order as the input Array' do + expect(collection.collection).to eq([issue1, issue2]) + end + end + + describe '#updatable_by_user' do + context 'using an admin user' do + it 'returns all issues' do + user = create(:admin) + + expect(collection.updatable_by_user(user)).to eq([issue1, issue2]) + end + end + + context 'using a user that has no access to the project' do + it 'returns no issues when the user is not an assignee or author' do + expect(collection.updatable_by_user(user)).to be_empty + end + + it 'returns the issues the user is assigned to' do + issue1.assignee = user + + expect(collection.updatable_by_user(user)).to eq([issue1]) + end + + it 'returns the issues for which the user is the author' do + issue1.author = user + + expect(collection.updatable_by_user(user)).to eq([issue1]) + end + end + + context 'using a user that has reporter access to the project' do + it 'returns the issues of the project' do + project.team << [user, :reporter] + + expect(collection.updatable_by_user(user)).to eq([issue1, issue2]) + end + end + + context 'using a user that is the owner of a project' do + it 'returns the issues of the project' do + expect(collection.updatable_by_user(project.namespace.owner)). + to eq([issue1, issue2]) + end + end + end + + describe '#visible_to' do + it 'is an alias for updatable_by_user' do + updatable_by_user = described_class.instance_method(:updatable_by_user) + visible_to = described_class.instance_method(:visible_to) + + expect(visible_to).to eq(updatable_by_user) + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 60d30eb7418..300425767ed 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,7 +22,7 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end - describe 'visible_to_user' do + describe '.visible_to_user' do let(:user) { create(:user) } let(:authorized_user) { create(:user) } let(:project) { create(:project, namespace: authorized_user.namespace) } @@ -102,17 +102,17 @@ describe Issue, models: true do it 'returns the merge request to close this issue' do mr - expect(issue.closed_by_merge_requests).to eq([mr]) + expect(issue.closed_by_merge_requests(mr.author)).to eq([mr]) end it "returns an empty array when the merge request is closed already" do closed_mr - expect(issue.closed_by_merge_requests).to eq([]) + expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([]) end it "returns an empty array when the current issue is closed already" do - expect(closed_issue.closed_by_merge_requests).to eq([]) + expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([]) end end @@ -218,7 +218,7 @@ describe Issue, models: true do source_project: subject.project, source_branch: "#{subject.iid}-branch" }) merge_request.create_cross_references!(user) - expect(subject.referenced_merge_requests).not_to be_empty + expect(subject.referenced_merge_requests(user)).not_to be_empty expect(subject.related_branches(user)).to eq([subject.to_branch_name]) end @@ -314,23 +314,6 @@ describe Issue, models: true do end describe '#visible_to_user?' do - context 'with a user' do - let(:user) { build(:user) } - let(:issue) { build(:issue) } - - it 'returns true when the issue is readable' do - expect(issue).to receive(:readable_by?).with(user).and_return(true) - - expect(issue.visible_to_user?(user)).to eq(true) - end - - it 'returns false when the issue is not readable' do - expect(issue).to receive(:readable_by?).with(user).and_return(false) - - expect(issue.visible_to_user?(user)).to eq(false) - end - end - context 'without a user' do let(:issue) { build(:issue) } @@ -346,9 +329,40 @@ describe Issue, models: true do expect(issue.visible_to_user?).to eq(false) end end - end - describe '#readable_by?' do + context 'with a user' do + let(:user) { build(:user) } + let(:issue) { build(:issue) } + + it 'returns true when the issue is readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(issue.visible_to_user?(user)).to eq(true) + end + + it 'returns false when the issue is not readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(issue.visible_to_user?(user)).to eq(false) + end + + it 'returns false when feature is disabled' do + expect(issue).not_to receive(:readable_by?) + + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + + expect(issue.visible_to_user?(user)).to eq(false) + end + + it 'returns false when restricted for members' do + expect(issue).not_to receive(:readable_by?) + + issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE) + + expect(issue.visible_to_user?(user)).to eq(false) + end + end + describe 'with a regular user that is not a team member' do let(:user) { create(:user) } @@ -358,13 +372,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns false for a confidential issue' do issue = build(:issue, project: project, confidential: true) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end @@ -375,13 +389,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end @@ -393,13 +407,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end end end @@ -410,26 +424,28 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(false) end context 'when the user is the project owner' do + before { project.team << [user, :master] } + it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -447,13 +463,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end @@ -467,13 +483,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end @@ -487,13 +503,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -505,13 +521,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end it 'returns true for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).to be_readable_by(user) + expect(issue.visible_to_user?(user)).to eq(true) end end end @@ -523,13 +539,13 @@ describe Issue, models: true do it 'returns true for a regular issue' do issue = build(:issue, project: project) - expect(issue).to be_publicly_visible + expect(issue).to be_truthy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end @@ -539,13 +555,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end @@ -555,13 +571,13 @@ describe Issue, models: true do it 'returns false for a regular issue' do issue = build(:issue, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end it 'returns false for a confidential issue' do issue = build(:issue, :confidential, project: project) - expect(issue).not_to be_publicly_visible + expect(issue).not_to be_falsy end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1067ff7bb4d..fb032a89d50 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -825,11 +825,8 @@ describe MergeRequest, models: true do end context 'when failed' do - before { allow(subject).to receive(:broken?) { false } } - - context 'when project settings restrict to merge only if build succeeds and build failed' do + context 'when #mergeable_ci_state? is false' do before do - project.only_allow_merge_if_build_succeeds = true allow(subject).to receive(:mergeable_ci_state?) { false } end @@ -837,6 +834,16 @@ describe MergeRequest, models: true do expect(subject.mergeable_state?).to be_falsey end end + + context 'when #mergeable_discussions_state? is false' do + before do + allow(subject).to receive(:mergeable_discussions_state?) { false } + end + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + end end end @@ -887,7 +894,49 @@ describe MergeRequest, models: true do end end - describe '#environments' do + describe '#mergeable_discussions_state?' do + let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) } + + context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do + let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) } + + context 'with all discussions resolved' do + before do + merge_request.discussions.each { |d| d.resolve!(merge_request.author) } + end + + it 'returns true' do + expect(merge_request.mergeable_discussions_state?).to be_truthy + end + end + + context 'with unresolved discussions' do + before do + merge_request.discussions.each(&:unresolve!) + end + + it 'returns false' do + expect(merge_request.mergeable_discussions_state?).to be_falsey + end + end + end + + context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do + let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: false) } + + context 'with unresolved discussions' do + before do + merge_request.discussions.each(&:unresolve!) + end + + it 'returns true' do + expect(merge_request.mergeable_discussions_state?).to be_truthy + end + end + end + end + + describe "#environments" do let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index ee0e38bd373..05ee4a08391 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -33,6 +33,41 @@ describe JiraService, models: true do end end + describe '#can_test?' do + let(:jira_service) { described_class.new } + + it 'returns false if username is blank' do + allow(jira_service).to receive_messages( + url: 'http://jira.example.com', + username: '', + password: '12345678' + ) + + expect(jira_service.can_test?).to be_falsy + end + + it 'returns false if password is blank' do + allow(jira_service).to receive_messages( + url: 'http://jira.example.com', + username: 'tester', + password: '' + ) + + expect(jira_service.can_test?).to be_falsy + end + + it 'returns true if password and username are present' do + jira_service = described_class.new + allow(jira_service).to receive_messages( + url: 'http://jira.example.com', + username: 'tester', + password: '12345678' + ) + + expect(jira_service.can_test?).to be_truthy + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project) } @@ -46,16 +81,19 @@ describe JiraService, models: true do service_hook: true, url: 'http://jira.example.com', username: 'gitlab_jira_username', - password: 'gitlab_jira_password' + password: 'gitlab_jira_password', + project_key: 'GitLabProject' ) @jira_service.save - project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' - @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' - @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' + project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' + @project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' + @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' + @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' - WebMock.stub_request(:get, project_url) + WebMock.stub_request(:get, @project_url) + WebMock.stub_request(:get, project_issues_url) WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) end @@ -99,6 +137,14 @@ describe JiraService, models: true do body: /this-is-a-custom-id/ ).once end + + context "when testing" do + it "tries to get jira project" do + @jira_service.execute(nil) + + expect(WebMock).to have_requested(:get, @project_url) + end + end end describe "Stored password invalidation" do diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb index 1368a2925e8..4f56bceda44 100644 --- a/spec/models/project_services/pipeline_email_service_spec.rb +++ b/spec/models/project_services/pipeline_email_service_spec.rb @@ -13,7 +13,7 @@ describe PipelinesEmailService do end before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe 'Validations' do @@ -23,14 +23,6 @@ describe PipelinesEmailService do end it { is_expected.to validate_presence_of(:recipients) } - - context 'when pusher is added' do - before do - subject.add_pusher = true - end - - it { is_expected.not_to validate_presence_of(:recipients) } - end end context 'when service is inactive' do @@ -66,8 +58,7 @@ describe PipelinesEmailService do end it 'sends email' do - sent_to = ActionMailer::Base.deliveries.flat_map(&:to) - expect(sent_to).to contain_exactly(recipient) + should_only_email(double(notification_email: recipient), kind: :bcc) end end @@ -79,7 +70,7 @@ describe PipelinesEmailService do end it 'does not send email' do - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0245897938c..0810d06b50f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -295,7 +295,7 @@ describe Project, models: true do end end - xdescribe "#new_issue_address" do + describe "#new_issue_address" do let(:project) { create(:empty_project, path: "somewhere") } let(:user) { create(:user) } @@ -305,8 +305,7 @@ describe Project, models: true do end it 'returns the address to create a new issue' do - token = user.authentication_token - address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab" + address = "p+#{project.path_with_namespace}+#{user.incoming_email_token}@gl.ab" expect(project.new_issue_address(user)).to eq(address) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 04b7d19d414..12989d4db53 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -362,6 +362,19 @@ describe Repository, models: true do expect(results.first).not_to start_with('fatal:') end + it 'properly handles when query is not present' do + results = repository.search_files('', 'master') + + expect(results).to match_array([]) + end + + it 'properly handles query when repo is empty' do + repository = create(:empty_project).repository + results = repository.search_files('test', 'master') + + expect(results).to match_array([]) + end + describe 'result' do subject { results.first } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ba47479a2e1..3b152e15b61 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1205,4 +1205,40 @@ describe User, models: true do expect(user.viewable_starred_projects).not_to include(private_project) end end + + describe '#projects_with_reporter_access_limited_to' do + let(:project1) { create(:project) } + let(:project2) { create(:project) } + let(:user) { create(:user) } + + before do + project1.team << [user, :reporter] + project2.team << [user, :guest] + end + + it 'returns the projects when using a single project ID' do + projects = user.projects_with_reporter_access_limited_to(project1.id) + + expect(projects).to eq([project1]) + end + + it 'returns the projects when using an Array of project IDs' do + projects = user.projects_with_reporter_access_limited_to([project1.id]) + + expect(projects).to eq([project1]) + end + + it 'returns the projects when using an ActiveRecord relation' do + projects = user. + projects_with_reporter_access_limited_to(Project.select(:id)) + + expect(projects).to eq([project1]) + end + + it 'does not return projects you do not have reporter access to' do + projects = user.projects_with_reporter_access_limited_to(project2.id) + + expect(projects).to be_empty + end + end end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb new file mode 100644 index 00000000000..7591bfd1471 --- /dev/null +++ b/spec/policies/issue_policy_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe IssuePolicy, models: true do + let(:user) { create(:user) } + + describe '#rules' do + context 'using a regular issue' do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:policies) { described_class.abilities(user, issue).to_set } + + context 'with a regular user' do + it 'includes the read_issue permission' do + expect(policies).to include(:read_issue) + end + + it 'does not include the admin_issue permission' do + expect(policies).not_to include(:admin_issue) + end + + it 'does not include the update_issue permission' do + expect(policies).not_to include(:update_issue) + end + end + + context 'with a user that is a project reporter' do + before do + project.team << [user, :reporter] + end + + it 'includes the read_issue permission' do + expect(policies).to include(:read_issue) + end + + it 'includes the admin_issue permission' do + expect(policies).to include(:admin_issue) + end + + it 'includes the update_issue permission' do + expect(policies).to include(:update_issue) + end + end + + context 'with a user that is a project guest' do + before do + project.team << [user, :guest] + end + + it 'includes the read_issue permission' do + expect(policies).to include(:read_issue) + end + + it 'does not include the admin_issue permission' do + expect(policies).not_to include(:admin_issue) + end + + it 'does not include the update_issue permission' do + expect(policies).not_to include(:update_issue) + end + end + end + + context 'using a confidential issue' do + let(:issue) { create(:issue, :confidential) } + + context 'with a regular user' do + let(:policies) { described_class.abilities(user, issue).to_set } + + it 'does not include the read_issue permission' do + expect(policies).not_to include(:read_issue) + end + + it 'does not include the admin_issue permission' do + expect(policies).not_to include(:admin_issue) + end + + it 'does not include the update_issue permission' do + expect(policies).not_to include(:update_issue) + end + end + + context 'with a user that is a project member' do + let(:policies) { described_class.abilities(user, issue).to_set } + + before do + issue.project.team << [user, :reporter] + end + + it 'includes the read_issue permission' do + expect(policies).to include(:read_issue) + end + + it 'includes the admin_issue permission' do + expect(policies).to include(:admin_issue) + end + + it 'includes the update_issue permission' do + expect(policies).to include(:update_issue) + end + end + + context 'without a user' do + let(:policies) { described_class.abilities(nil, issue).to_set } + + it 'does not include the read_issue permission' do + expect(policies).not_to include(:read_issue) + end + + it 'does not include the admin_issue permission' do + expect(policies).not_to include(:admin_issue) + end + + it 'does not include the update_issue permission' do + expect(policies).not_to include(:update_issue) + end + end + end + end +end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 46641fcd846..2ff90b6deac 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -6,6 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:label1) { create(:label, title: 'label1', project: project) } + let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } before do project.team << [user, :master] @@ -16,13 +17,27 @@ describe API::API, api: true do group = create(:group) group_label = create(:group_label, group: group) project.update(group: group) + expected_keys = [ + 'id', 'name', 'color', 'description', + 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count', + 'subscribed', 'priority' + ] get api("/projects/#{project.id}/labels", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array - expect(json_response.size).to eq(2) - expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name]) + expect(json_response.size).to eq(3) + expect(json_response.first.keys).to match_array expected_keys + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) + expect(json_response.last['name']).to eq(label1.name) + expect(json_response.last['color']).to be_present + expect(json_response.last['description']).to be_nil + expect(json_response.last['open_issues_count']).to eq(0) + expect(json_response.last['closed_issues_count']).to eq(0) + expect(json_response.last['open_merge_requests_count']).to eq(0) + expect(json_response.last['priority']).to be_nil + expect(json_response.last['subscribed']).to be_falsey end end @@ -31,21 +46,39 @@ describe API::API, api: true do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAABB', - description: 'test' + description: 'test', + priority: 2 + expect(response).to have_http_status(201) expect(json_response['name']).to eq('Foo') expect(json_response['color']).to eq('#FFAABB') expect(json_response['description']).to eq('test') + expect(json_response['priority']).to eq(2) end it 'returns created label when only required params' do post api("/projects/#{project.id}/labels", user), name: 'Foo & Bar', color: '#FFAABB' + expect(response.status).to eq(201) expect(json_response['name']).to eq('Foo & Bar') expect(json_response['color']).to eq('#FFAABB') expect(json_response['description']).to be_nil + expect(json_response['priority']).to be_nil + end + + it 'creates a prioritized label' do + post api("/projects/#{project.id}/labels", user), + name: 'Foo & Bar', + color: '#FFAABB', + priority: 3 + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('Foo & Bar') + expect(json_response['color']).to eq('#FFAABB') + expect(json_response['description']).to be_nil + expect(json_response['priority']).to eq(3) end it 'returns a 400 bad request if name not given' do @@ -82,7 +115,29 @@ describe API::API, api: true do expect(json_response['message']['title']).to eq(['is invalid']) end - it 'returns 409 if label already exists' do + it 'returns 409 if label already exists in group' do + group = create(:group) + group_label = create(:group_label, group: group) + project.update(group: group) + + post api("/projects/#{project.id}/labels", user), + name: group_label.name, + color: '#FFAABB' + + expect(response).to have_http_status(409) + expect(json_response['message']).to eq('Label already exists') + end + + it 'returns 400 for invalid priority' do + post api("/projects/#{project.id}/labels", user), + name: 'Foo', + color: '#FFAAFFFF', + priority: 'foo' + + expect(response).to have_http_status(400) + end + + it 'returns 409 if label already exists in project' do post api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFAABB' @@ -142,11 +197,43 @@ describe API::API, api: true do it 'returns 200 if description is changed' do put api("/projects/#{project.id}/labels", user), - name: 'label1', + name: 'bug', description: 'test' + expect(response).to have_http_status(200) - expect(json_response['name']).to eq(label1.name) + expect(json_response['name']).to eq(priority_label.name) expect(json_response['description']).to eq('test') + expect(json_response['priority']).to eq(3) + end + + it 'returns 200 if priority is changed' do + put api("/projects/#{project.id}/labels", user), + name: 'bug', + priority: 10 + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(priority_label.name) + expect(json_response['priority']).to eq(10) + end + + it 'returns 200 if a priority is added' do + put api("/projects/#{project.id}/labels", user), + name: 'label1', + priority: 3 + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['priority']).to eq(3) + end + + it 'returns 200 if the priority is removed' do + put api("/projects/#{project.id}/labels", user), + name: priority_label.name, + priority: nil + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(priority_label.name) + expect(json_response['priority']).to be_nil end it 'returns 404 if label does not exist' do @@ -165,7 +252,7 @@ describe API::API, api: true do it 'returns 400 if no new parameters given' do put api("/projects/#{project.id}/labels", user), name: 'label1' expect(response).to have_http_status(400) - expect(json_response['error']).to eq('new_name, color, description are missing, '\ + expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\ 'at least one parameter must be provided') end @@ -193,6 +280,14 @@ describe API::API, api: true do expect(response).to have_http_status(400) expect(json_response['message']['color']).to eq(['must be a valid color code']) end + + it 'returns 400 for invalid priority' do + post api("/projects/#{project.id}/labels", user), + name: 'Foo', + priority: 'foo' + + expect(response).to have_http_status(400) + end end describe "POST /projects/:id/labels/:label_id/subscription" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 973928d007a..3c8f0ac531a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -256,7 +256,8 @@ describe API::API, api: true do merge_requests_enabled: false, wiki_enabled: false, only_allow_merge_if_build_succeeds: false, - request_access_enabled: true + request_access_enabled: true, + only_allow_merge_if_all_discussions_are_resolved: false }) post api('/projects', user), project @@ -327,6 +328,22 @@ describe API::API, api: true do expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy end + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end + context 'when a visibility level is restricted' do before do @project = attributes_for(:project, { public: true }) @@ -448,6 +465,22 @@ describe API::API, api: true do post api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy end + + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end end describe "POST /projects/:id/uploads" do @@ -509,6 +542,7 @@ describe API::API, api: true do expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) end it 'returns a project by path name' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index f685a3685e6..6c9df21f598 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -52,6 +52,12 @@ describe API::API, api: true do expect(response).to have_http_status(400) end + it "responds with 400 if url is invalid" do + post api("/hooks", admin), url: 'hp://mep.mep' + + expect(response).to have_http_status(400) + end + it "does not create new hook without url" do expect do post api("/hooks", admin) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 27f0fd22ae6..f1728d61def 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -115,6 +115,38 @@ describe 'Git HTTP requests', lib: true do end.to raise_error(JWT::DecodeError) end end + + context 'when the repo is public' do + context 'but the repo is disabled' do + it 'does not allow to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:unauthorized) + end + end + end + + context 'but the repo is enabled' do + it 'allows to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:ok) + end + end + end + + context 'but only project members are allowed' do + it 'does not allow to clone the repo' do + project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE) + + download("#{project.path_with_namespace}.git", {}) do |response| + expect(response).to have_http_status(:unauthorized) + end + end + end + end end context "when the project is private" do diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index f0ef155bd7b..a3e7844b2f3 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -20,7 +20,7 @@ describe JwtController do end end - context 'when using authorized request' do + context 'when using authenticated request' do context 'using CI token' do let(:build) { create(:ci_build, :running) } let(:project) { build.project } @@ -65,7 +65,7 @@ describe JwtController do let(:access_token) { create(:personal_access_token, user: user) } let(:headers) { { authorization: credentials(user.username, access_token.token) } } - it 'rejects the authorization attempt' do + it 'accepts the authorization attempt' do expect(response).to have_http_status(200) end end @@ -81,6 +81,20 @@ describe JwtController do end end + context 'when using unauthenticated request' do + it 'accepts the authorization attempt' do + get '/jwt/auth', parameters + + expect(response).to have_http_status(200) + end + + it 'allows read access' do + expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities) + + get '/jwt/auth', parameters + end + end + context 'unknown service' do subject! { get '/jwt/auth', service: 'unknown' } diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index c18a2d55e43..61dca5d5a62 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -266,13 +266,13 @@ describe "Groups", "routing" do end it "also display group#show on the short path" do - allow(Group).to receive(:find_by_path).and_return(true) + allow(Group).to receive(:find_by).and_return(true) expect(get('/1')).to route_to('groups#show', id: '1') end it "also display group#show with dot in the path" do - allow(Group).to receive(:find_by_path).and_return(true) + allow(Group).to receive(:find_by).and_return(true) expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot') end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb new file mode 100644 index 00000000000..2734f5bedca --- /dev/null +++ b/spec/serializers/build_entity_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe BuildEntity do + let(:entity) do + described_class.new(build, request: double) + end + + subject { entity.as_json } + + context 'when build is a regular job' do + let(:build) { create(:ci_build) } + + it 'contains url to build page and retry action' do + expect(subject).to include(:build_url, :retry_url) + expect(subject).not_to include(:play_url) + end + + it 'does not contain sensitive information' do + expect(subject).not_to include(/token/) + expect(subject).not_to include(/variables/) + end + end + + context 'when build is a manual action' do + let(:build) { create(:ci_build, :manual) } + + it 'contains url to play action' do + expect(subject).to include(:play_url) + end + end +end diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb new file mode 100644 index 00000000000..628e35c9a28 --- /dev/null +++ b/spec/serializers/commit_entity_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe CommitEntity do + let(:entity) do + described_class.new(commit, request: request) + end + + let(:request) { double('request') } + let(:project) { create(:project) } + let(:commit) { project.commit } + + subject { entity.as_json } + + before do + allow(request).to receive(:project).and_return(project) + end + + context 'when commit author is a user' do + before do + create(:user, email: commit.author_email) + end + + it 'contains information about user' do + expect(subject.fetch(:author)).not_to be_nil + end + end + + context 'when commit author is not a user' do + it 'does not contain author details' do + expect(subject.fetch(:author)).to be_nil + end + end + + it 'contains commit URL' do + expect(subject).to include(:commit_url) + end + + it 'needs to receive project in the request' do + expect(request).to receive(:project) + .and_return(project) + + subject + end +end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb new file mode 100644 index 00000000000..51b6de91571 --- /dev/null +++ b/spec/serializers/deployment_entity_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DeploymentEntity do + let(:entity) do + described_class.new(deployment, request: double) + end + + let(:deployment) { create(:deployment) } + + subject { entity.as_json } + + it 'exposes internal deployment id' do + expect(subject).to include(:iid) + end + + it 'exposes nested information about branch' do + expect(subject[:ref][:name]).to eq 'master' + expect(subject[:ref][:ref_url]).not_to be_empty + end +end diff --git a/spec/serializers/entity_request_spec.rb b/spec/serializers/entity_request_spec.rb new file mode 100644 index 00000000000..86654adfd54 --- /dev/null +++ b/spec/serializers/entity_request_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe EntityRequest do + subject do + described_class.new(user: 'user', project: 'some project') + end + + describe 'methods created' do + it 'defines accessible attributes' do + expect(subject.user).to eq 'user' + expect(subject.project).to eq 'some project' + end + + it 'raises error when attribute is not defined' do + expect { subject.some_method }.to raise_error NoMethodError + end + end +end diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb new file mode 100644 index 00000000000..4ca8c299147 --- /dev/null +++ b/spec/serializers/environment_entity_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe EnvironmentEntity do + let(:entity) do + described_class.new(environment, request: double) + end + + let(:environment) { create(:environment) } + subject { entity.as_json } + + it 'exposes latest deployment' do + expect(subject).to include(:last_deployment) + end + + it 'exposes core elements of environment' do + expect(subject).to include(:id, :name, :state, :environment_url) + end +end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb new file mode 100644 index 00000000000..37bc086826c --- /dev/null +++ b/spec/serializers/environment_serializer_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe EnvironmentSerializer do + let(:serializer) do + described_class + .new(user: user, project: project) + .represent(resource) + end + + let(:json) { serializer.as_json } + let(:user) { create(:user) } + let(:project) { create(:project) } + + context 'when there is a single object provided' do + before do + create(:ci_build, :manual, name: 'manual1', + pipeline: deployable.pipeline) + end + + let(:deployment) do + create(:deployment, deployable: deployable, + user: user, + project: project, + sha: project.commit.id) + end + + let(:deployable) { create(:ci_build) } + let(:resource) { deployment.environment } + + it 'it generates payload for single object' do + expect(json).to be_an_instance_of Hash + end + + it 'contains important elements of environment' do + expect(json) + .to include(:name, :external_url, :environment_url, :last_deployment) + end + + it 'contains relevant information about last deployment' do + last_deployment = json.fetch(:last_deployment) + + expect(last_deployment) + .to include(:ref, :user, :commit, :deployable, :manual_actions) + end + end + + context 'when there is a collection of objects provided' do + let(:project) { create(:empty_project) } + let(:resource) { create_list(:environment, 2) } + + it 'contains important elements of environment' do + expect(json.first) + .to include(:last_deployment, :name, :external_url) + end + + it 'generates payload for collection' do + expect(json).to be_an_instance_of Array + end + end +end diff --git a/spec/serializers/user_entity_spec.rb b/spec/serializers/user_entity_spec.rb new file mode 100644 index 00000000000..c5d11cbcf5e --- /dev/null +++ b/spec/serializers/user_entity_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe UserEntity do + let(:entity) { described_class.new(user) } + let(:user) { create(:user) } + subject { entity.as_json } + + it 'exposes user name and login' do + expect(subject).to include(:username, :name) + end + + it 'does not expose passwords' do + expect(subject).not_to include(/password/) + end + + it 'does not expose tokens' do + expect(subject).not_to include(/token/) + end + + it 'does not expose 2FA OTPs' do + expect(subject).not_to include(/otp/) + end +end diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb deleted file mode 100644 index 288302cc94f..00000000000 --- a/spec/services/ci/send_pipeline_notification_service_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Ci::SendPipelineNotificationService, services: true do - let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit('master').sha, - user: user, - status: status) - end - - let(:project) { create(:project) } - let(:user) { create(:user) } - - subject{ described_class.new(pipeline) } - - describe '#execute' do - before do - reset_delivered_emails! - end - - shared_examples 'sending emails' do - it 'sends an email to pipeline user' do - perform_enqueued_jobs do - subject.execute([user.email]) - end - - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include(email_subject) - expect(email.to).to eq([user.email]) - end - end - - context 'with success pipeline' do - let(:status) { 'success' } - let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } - - it_behaves_like 'sending emails' - end - - context 'with failed pipeline' do - let(:status) { 'failed' } - let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } - - it_behaves_like 'sending emails' - end - end -end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 45bc44ba172..cea7e6429f9 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -302,6 +302,9 @@ describe GitPushService, services: true do author_email: commit_author.email ) + allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + and_return(commit) + allow(project.repository).to receive(:commits_between).and_return([commit]) end @@ -357,6 +360,9 @@ describe GitPushService, services: true do committed_date: commit_time ) + allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + and_return(commit) + allow(project.repository).to receive(:commits_between).and_return([commit]) end @@ -393,6 +399,9 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) + allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit). + and_return(closing_commit) + project.team << [commit_author, :master] end @@ -538,9 +547,16 @@ describe GitPushService, services: true do let(:housekeeping) { Projects::HousekeepingService.new(project) } before do + # Flush any raw Redis data stored by the housekeeping code. + Gitlab::Redis.with { |conn| conn.flushall } + allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping) end + after do + Gitlab::Redis.with { |conn| conn.flushall } + end + it 'does not perform housekeeping when not needed' do expect(housekeeping).not_to receive(:execute) diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 5dfb33f4b28..4465f22a001 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -15,10 +15,39 @@ describe Issues::CloseService, services: true do end describe '#execute' do + let(:service) { described_class.new(project, user) } + + it 'checks if the user is authorized to update the issue' do + expect(service).to receive(:can?).with(user, :update_issue, issue). + and_call_original + + service.execute(issue) + end + + it 'does not close the issue when the user is not authorized to do so' do + allow(service).to receive(:can?).with(user, :update_issue, issue). + and_return(false) + + expect(service).not_to receive(:close_issue) + expect(service.execute(issue)).to eq(issue) + end + + it 'closes the issue when the user is authorized to do so' do + allow(service).to receive(:can?).with(user, :update_issue, issue). + and_return(true) + + expect(service).to receive(:close_issue). + with(issue, commit: nil, notifications: true, system_note: true) + + service.execute(issue) + end + end + + describe '#close_issue' do context "valid params" do before do perform_enqueued_jobs do - described_class.new(project, user).execute(issue) + described_class.new(project, user).close_issue(issue) end end @@ -41,24 +70,12 @@ describe Issues::CloseService, services: true do end end - context 'current user is not authorized to close issue' do - before do - perform_enqueued_jobs do - described_class.new(project, guest).execute(issue) - end - end - - it 'does not close the issue' do - expect(issue).to be_open - end - end - context 'when issue is not confidential' do it 'executes issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks) - described_class.new(project, user).execute(issue) + described_class.new(project, user).close_issue(issue) end end @@ -69,14 +86,14 @@ describe Issues::CloseService, services: true do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks) - described_class.new(project, user).execute(issue) + described_class.new(project, user).close_issue(issue) end end context 'external issue tracker' do before do allow(project).to receive(:default_issues_tracker?).and_return(false) - described_class.new(project, user).execute(issue) + described_class.new(project, user).close_issue(issue) end it { expect(issue).to be_valid } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 699b9925b4e..8ce35354c22 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -17,7 +17,7 @@ describe NotificationService, services: true do it 'sends no emails when no new mentions are present' do send_notifications - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end it 'emails new mentions with a watch level higher than participant' do @@ -27,7 +27,7 @@ describe NotificationService, services: true do it 'does not email new mentions with a watch level equal to or less than participant' do send_notifications(@u_participating, @u_mentioned) - expect(ActionMailer::Base.deliveries).to be_empty + should_not_email_anyone end end @@ -79,7 +79,7 @@ describe NotificationService, services: true do # Ensure create SentNotification by noteable = issue 6 times, not noteable = note expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_note(note) @@ -111,7 +111,7 @@ describe NotificationService, services: true do context 'participating' do context 'by note' do before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! note.author = @u_lazy_participant note.save notification.new_note(note) @@ -134,7 +134,7 @@ describe NotificationService, services: true do @u_watcher.notification_settings_for(note.project).participating! @u_watcher.notification_settings_for(note.project.group).global! update_custom_notification(:new_note, @u_custom_global) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end it do @@ -173,7 +173,7 @@ describe NotificationService, services: true do expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_note(note) @@ -196,7 +196,7 @@ describe NotificationService, services: true do before do build_team(note.project) note.project.team << [note.author, :master] - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_note' do @@ -238,7 +238,7 @@ describe NotificationService, services: true do before do build_team(note.project) note.project.team << [note.author, :master] - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_note' do @@ -273,7 +273,7 @@ describe NotificationService, services: true do before do build_team(note.project) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) update_custom_notification(:new_note, @u_guest_custom, project) update_custom_notification(:new_note, @u_custom_global) @@ -348,7 +348,7 @@ describe NotificationService, services: true do before do build_team(issue.project) add_users_with_subscription(issue.project, issue) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! update_custom_notification(:new_issue, @u_guest_custom, project) update_custom_notification(:new_issue, @u_custom_global) end @@ -408,7 +408,7 @@ describe NotificationService, services: true do label.toggle_subscription(guest) label.toggle_subscription(admin) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.new_issue(confidential_issue, @u_disabled) @@ -604,7 +604,7 @@ describe NotificationService, services: true do label_2.toggle_subscription(guest) label_2.toggle_subscription(admin) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! notification.relabeled_issue(confidential_issue, [label_2], @u_disabled) @@ -733,7 +733,7 @@ describe NotificationService, services: true do add_users_with_subscription(merge_request.target_project, merge_request) update_custom_notification(:new_merge_request, @u_guest_custom, project) update_custom_notification(:new_merge_request, @u_custom_global) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#new_merge_request' do @@ -1111,7 +1111,7 @@ describe NotificationService, services: true do before do build_team(project) - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end describe '#project_was_moved' do diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index cf90b33dfb4..57a5aa5cedc 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -14,8 +14,10 @@ describe Projects::HousekeepingService do describe '#execute' do it 'enqueues a sidekiq job' do - expect(subject).to receive(:try_obtain_lease).and_return(true) - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id) + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + expect(subject).to receive(:lease_key).and_return(:the_lease_key) + expect(subject).to receive(:task).and_return(:the_task) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid) subject.execute expect(project.reload.pushes_since_gc).to eq(0) @@ -58,4 +60,26 @@ describe Projects::HousekeepingService do end.to change { project.pushes_since_gc }.from(0).to(1) end end + + it 'uses all three kinds of housekeeping we offer' do + allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:lease_key).and_return(:the_lease_key) + + # At push 200 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid). + exactly(1).times + # At push 50, 100, 150 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid). + exactly(3).times + # At push 10, 20, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid). + exactly(16).times + + 201.times do + subject.increment! + subject.execute if subject.needed? + end + + expect(project.pushes_since_gc).to eq(1) + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b2ca856f89f..73cf4c9a24c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,7 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature + config.include SearchHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include TestEnv diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 62a5b46d47b..75c95d70951 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -49,7 +49,8 @@ module CycleAnalyticsHelpers end def merge_merge_requests_closing_issue(issue) - merge_requests = issue.closed_by_merge_requests + merge_requests = issue.closed_by_merge_requests(user) + merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) } end diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb index 0bfc4685532..3e979f2f470 100644 --- a/spec/support/email_helpers.rb +++ b/spec/support/email_helpers.rb @@ -1,23 +1,33 @@ module EmailHelpers - def sent_to_user?(user) - ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1 + def sent_to_user?(user, recipients = email_recipients) + recipients.include?(user.notification_email) end def reset_delivered_emails! ActionMailer::Base.deliveries.clear end - def should_only_email(*users) - users.each {|user| should_email(user) } - recipients = ActionMailer::Base.deliveries.flat_map(&:to) + def should_only_email(*users, kind: :to) + recipients = email_recipients(kind: kind) + + users.each { |user| should_email(user, recipients) } + expect(recipients.count).to eq(users.count) end - def should_email(user) - expect(sent_to_user?(user)).to be_truthy + def should_email(user, recipients = email_recipients) + expect(sent_to_user?(user, recipients)).to be_truthy end - def should_not_email(user) - expect(sent_to_user?(user)).to be_falsey + def should_not_email(user, recipients = email_recipients) + expect(sent_to_user?(user, recipients)).to be_falsey + end + + def should_not_email_anyone + expect(ActionMailer::Base.deliveries).to be_empty + end + + def email_recipients(kind: :to) + ActionMailer::Base.deliveries.flat_map(&kind) end end diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index 3956d05060b..49867aa5cc4 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do let(:new_user_address) { 'newguy@example.com' } before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! email = recipient.emails.create(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}") diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb new file mode 100644 index 00000000000..4621d17549b --- /dev/null +++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb @@ -0,0 +1,56 @@ +shared_examples 'project features apply to issuables' do |klass| + let(:described_class) { klass } + + let(:group) { create(:group) } + let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user } + let(:user_outside_group) { create(:user) } + + let(:project) { create(:empty_project, :public, project_args) } + + def project_args + feature = "#{described_class.model_name.plural}_access_level".to_sym + + args = { group: group } + args[feature] = access_level + + args + end + + before do + _ = issuable + login_as(user) + visit path + end + + context 'public access level' do + let(:access_level) { ProjectFeature::ENABLED } + + context 'group member' do + let(:user) { user_in_group } + + it { expect(page).to have_content(issuable.title) } + end + + context 'non-member' do + let(:user) { user_outside_group } + + it { expect(page).to have_content(issuable.title) } + end + end + + context 'private access level' do + let(:access_level) { ProjectFeature::PRIVATE } + + context 'group member' do + let(:user) { user_in_group } + + it { expect(page).to have_content(issuable.title) } + end + + context 'non-member' do + let(:user) { user_outside_group } + + it { expect(page).not_to have_content(issuable.title) } + end + end +end diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb new file mode 100644 index 00000000000..8eb74635a60 --- /dev/null +++ b/spec/support/reference_parser_shared_examples.rb @@ -0,0 +1,43 @@ +RSpec.shared_examples "referenced feature visibility" do |*related_features| + let(:feature_fields) do + related_features.map { |feature| (feature + "_access_level").to_sym } + end + + before { link['data-project'] = project.id.to_s } + + context "when feature is disabled" do + it "does not create reference" do + set_features_fields_to(ProjectFeature::DISABLED) + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context "when feature is enabled only for team members" do + before { set_features_fields_to(ProjectFeature::PRIVATE) } + + it "does not create reference for non member" do + non_member = create(:user) + + expect(subject.nodes_visible_to_user(non_member, [link])).to eq([]) + end + + it "creates reference for member" do + project.team << [user, :developer] + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + + context "when feature is enabled" do + # The project is public + it "creates reference" do + set_features_fields_to(ProjectFeature::ENABLED) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + + def set_features_fields_to(visibility_level) + feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) } + end +end diff --git a/spec/support/search_helpers.rb b/spec/support/search_helpers.rb new file mode 100644 index 00000000000..abbbb636d66 --- /dev/null +++ b/spec/support/search_helpers.rb @@ -0,0 +1,5 @@ +module SearchHelpers + def select_filter(name) + find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c79975d8667..778e665500d 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -204,20 +204,18 @@ module TestEnv end def set_repo_refs(repo_path, branch_sha) + instructions = branch_sha.map {|branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00" + update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) + reset = proc do + IO.popen(update_refs, "w") {|io| io.write(instructions) } + $?.success? + end + Dir.chdir(repo_path) do - branch_sha.each do |branch, sha| - # Try to reset without fetching to avoid using the network. - reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha}) - unless system(*reset) - if system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - unless system(*reset) - raise 'The fetched test seed '\ - 'does not contain the required revision.' - end - else - raise 'Could not fetch test seed repository.' - end - end + # Try to reset without fetching to avoid using the network. + unless reset.call + raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) + raise 'The fetched test seed does not contain the required revision.' unless reset.call end end end diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb new file mode 100644 index 00000000000..538ff952bf4 --- /dev/null +++ b/spec/tasks/gitlab/check_rake_spec.rb @@ -0,0 +1,51 @@ +require 'rake_helper' + +describe 'gitlab:ldap:check rake task' do + include LdapHelpers + + before do + Rake.application.rake_require 'tasks/gitlab/check' + + stub_warn_user_is_not_gitlab + end + + context 'when LDAP is not enabled' do + it 'does not attempt to bind or search for users' do + expect(Gitlab::LDAP::Config).not_to receive(:providers) + expect(Gitlab::LDAP::Adapter).not_to receive(:open) + + run_rake_task('gitlab:ldap:check') + end + end + + context 'when LDAP is enabled' do + let(:ldap) { double(:ldap) } + let(:adapter) { ldap_adapter('ldapmain', ldap) } + + before do + allow(Gitlab::LDAP::Config) + .to receive_messages( + enabled?: true, + providers: ['ldapmain'] + ) + allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter) + allow(adapter).to receive(:users).and_return([]) + end + + it 'attempts to bind using credentials' do + stub_ldap_config(has_auth?: true) + + expect(ldap).to receive(:bind) + + run_rake_task('gitlab:ldap:check') + end + + it 'searches for 100 LDAP users' do + stub_ldap_config(uid: 'uid') + + expect(adapter).to receive(:users).with('uid', '*', 100) + + run_rake_task('gitlab:ldap:check') + end + end +end diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb index 788b92c1b84..a1aa336361a 100644 --- a/spec/workers/build_email_worker_spec.rb +++ b/spec/workers/build_email_worker_spec.rb @@ -24,7 +24,7 @@ describe BuildEmailWorker do end it "gracefully handles an input SMTP error" do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError) subject.perform(build.id, [user.email], data.stringify_keys) diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 036d037f3f9..fc652f6f4c3 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -87,7 +87,7 @@ describe EmailsOnPushWorker do context "when there is an SMTP error" do before do - ActionMailer::Base.deliveries.clear + reset_delivered_emails! allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) allow(subject).to receive_message_chain(:logger, :info) perform @@ -112,7 +112,7 @@ describe EmailsOnPushWorker do original.call(Mail.new(mail.encoded)) end - ActionMailer::Base.deliveries.clear + reset_delivered_emails! end it "sends the mail to each of the recipients" do diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index c9f5aae0815..e471a68a49a 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -1,3 +1,5 @@ +require 'fileutils' + require 'spec_helper' describe GitGarbageCollectWorker do @@ -6,16 +8,12 @@ describe GitGarbageCollectWorker do subject { GitGarbageCollectWorker.new } - before do - allow(subject).to receive(:gitlab_shell).and_return(shell) - end - describe "#perform" do - it "runs `git gc`" do - expect(shell).to receive(:gc).with( - project.repository_storage_path, - project.path_with_namespace). - and_return(true) + it "flushes ref caches when the task is 'gc'" do + expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) + expect(Gitlab::Popen).to receive(:popen). + with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:branch_count).and_call_original @@ -23,5 +21,110 @@ describe GitGarbageCollectWorker do subject.perform(project.id) end + + shared_examples 'gc tasks' do + before { allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) } + + it 'incremental repack adds a new packfile' do + create_objects(project) + before_packs = packs(project) + + expect(before_packs.count).to be >= 1 + + subject.perform(project.id, 'incremental_repack') + after_packs = packs(project) + + # Exactly one new pack should have been created + expect(after_packs.count).to eq(before_packs.count + 1) + + # Previously existing packs are still around + expect(before_packs & after_packs).to eq(before_packs) + end + + it 'full repack consolidates into 1 packfile' do + create_objects(project) + subject.perform(project.id, 'incremental_repack') + before_packs = packs(project) + + expect(before_packs.count).to be >= 2 + + subject.perform(project.id, 'full_repack') + after_packs = packs(project) + + expect(after_packs.count).to eq(1) + + # Previously existing packs should be gone now + expect(after_packs - before_packs).to eq(after_packs) + + expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) + end + + it 'gc consolidates into 1 packfile and updates packed-refs' do + create_objects(project) + before_packs = packs(project) + before_packed_refs = packed_refs(project) + + expect(before_packs.count).to be >= 1 + + subject.perform(project.id, 'gc') + after_packed_refs = packed_refs(project) + after_packs = packs(project) + + expect(after_packs.count).to eq(1) + + # Previously existing packs should be gone now + expect(after_packs - before_packs).to eq(after_packs) + + # The packed-refs file should have been updated during 'git gc' + expect(before_packed_refs).not_to eq(after_packed_refs) + + expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) + end + end + + context 'with bitmaps enabled' do + let(:bitmaps_enabled) { true } + + include_examples 'gc tasks' + end + + context 'with bitmaps disabled' do + let(:bitmaps_enabled) { false } + + include_examples 'gc tasks' + end + end + + # Create a new commit on a random new branch + def create_objects(project) + rugged = project.repository.rugged + old_commit = rugged.branches.first.target + new_commit_sha = Rugged::Commit.create( + rugged, + message: "hello world #{SecureRandom.hex(6)}", + author: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'), + committer: Gitlab::Git::committer_hash(email: 'foo@bar', name: 'baz'), + tree: old_commit.tree, + parents: [old_commit], + ) + project.repository.update_ref!( + "refs/heads/#{SecureRandom.hex(6)}", + new_commit_sha, + Gitlab::Git::BLANK_SHA + ) + end + + def packs(project) + Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"] + end + + def packed_refs(project) + path = "#{project.repository.path_to_repo}/packed-refs" + FileUtils.touch(path) + File.read(path) + end + + def bitmap_path(pack) + pack.sub(/\.pack\z/, '.bitmap') end end diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb new file mode 100644 index 00000000000..d487a719680 --- /dev/null +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' + +describe PipelineNotificationWorker do + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: project.commit('master').sha, + user: pusher, + status: status) + end + + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:pusher) { user } + let(:watcher) { pusher } + + describe '#execute' do + before do + reset_delivered_emails! + pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER] + end + + context 'when watcher has developer access' do + before do + pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER] + end + + shared_examples 'sending emails' do + it 'sends emails' do + perform_enqueued_jobs do + subject.perform(pipeline.id) + end + + emails = ActionMailer::Base.deliveries + actual = emails.flat_map(&:bcc).sort + expected_receivers = receivers.map(&:email).uniq.sort + + expect(actual).to eq(expected_receivers) + expect(emails.size).to eq(1) + expect(emails.last.subject).to include(email_subject) + end + end + + context 'with success pipeline' do + let(:status) { 'success' } + let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" } + let(:receivers) { [pusher, watcher] } + + it_behaves_like 'sending emails' + + context 'with pipeline from someone else' do + let(:pusher) { create(:user) } + let(:watcher) { user } + + context 'with success pipeline notification on' do + before do + watcher.global_notification_setting. + update(level: 'custom', success_pipeline: true) + end + + it_behaves_like 'sending emails' + end + + context 'with success pipeline notification off' do + let(:receivers) { [pusher] } + + before do + watcher.global_notification_setting. + update(level: 'custom', success_pipeline: false) + end + + it_behaves_like 'sending emails' + end + end + + context 'with failed pipeline' do + let(:status) { 'failed' } + let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } + + it_behaves_like 'sending emails' + + context 'with pipeline from someone else' do + let(:pusher) { create(:user) } + let(:watcher) { user } + + context 'with failed pipeline notification on' do + before do + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: true) + end + + it_behaves_like 'sending emails' + end + + context 'with failed pipeline notification off' do + let(:receivers) { [pusher] } + + before do + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: false) + end + + it_behaves_like 'sending emails' + end + end + end + end + end + + context 'when watcher has no read_build access' do + let(:status) { 'failed' } + let(:email_subject) { "Pipeline ##{pipeline.id} has failed" } + let(:watcher) { create(:user) } + + before do + pipeline.project.team << [watcher, Gitlab::Access::GUEST] + + watcher.global_notification_setting. + update(level: 'custom', failed_pipeline: true) + + perform_enqueued_jobs do + subject.perform(pipeline.id) + end + end + + it 'does not send emails' do + should_only_email(pusher, kind: :bcc) + end + end + end +end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb new file mode 100644 index 00000000000..3e4fee42240 --- /dev/null +++ b/spec/workers/process_commit_worker_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe ProcessCommitWorker do + let(:worker) { described_class.new } + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, author: user) } + let(:commit) { project.commit } + + describe '#perform' do + it 'does not process the commit when the project does not exist' do + expect(worker).not_to receive(:close_issues) + + worker.perform(-1, user.id, commit.id) + end + + it 'does not process the commit when the user does not exist' do + expect(worker).not_to receive(:close_issues) + + worker.perform(project.id, -1, commit.id) + end + + it 'does not process the commit when the commit no longer exists' do + expect(worker).not_to receive(:close_issues) + + worker.perform(project.id, user.id, 'this-should-does-not-exist') + end + + it 'processes the commit message' do + expect(worker).to receive(:process_commit_message).and_call_original + + worker.perform(project.id, user.id, commit.id) + end + + it 'updates the issue metrics' do + expect(worker).to receive(:update_issue_metrics).and_call_original + + worker.perform(project.id, user.id, commit.id) + end + end + + describe '#process_commit_message' do + context 'when pushing to the default branch' do + it 'closes issues that should be closed per the commit message' do + allow(commit).to receive(:safe_message). + and_return("Closes #{issue.to_reference}") + + expect(worker).to receive(:close_issues). + with(project, user, user, commit, [issue]) + + worker.process_commit_message(project, commit, user, user, true) + end + end + + context 'when pushing to a non-default branch' do + it 'does not close any issues' do + allow(commit).to receive(:safe_message). + and_return("Closes #{issue.to_reference}") + + expect(worker).not_to receive(:close_issues) + + worker.process_commit_message(project, commit, user, user, false) + end + end + + it 'creates cross references' do + expect(commit).to receive(:create_cross_references!) + + worker.process_commit_message(project, commit, user, user) + end + end + + describe '#close_issues' do + context 'when the user can update the issues' do + it 'closes the issues' do + worker.close_issues(project, user, user, commit, [issue]) + + issue.reload + + expect(issue.closed?).to eq(true) + end + end + + context 'when the user can not update the issues' do + it 'does not close the issues' do + other_user = create(:user) + + worker.close_issues(project, other_user, other_user, commit, [issue]) + + issue.reload + + expect(issue.closed?).to eq(false) + end + end + end + + describe '#update_issue_metrics' do + it 'updates any existing issue metrics' do + allow(commit).to receive(:safe_message). + and_return("Closes #{issue.to_reference}") + + worker.update_issue_metrics(commit, user) + + metric = Issue::Metrics.first + + expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date) + end + end +end diff --git a/vendor/assets/javascripts/jquery.timeago.js b/vendor/assets/javascripts/jquery.timeago.js deleted file mode 100644 index de76cdd2ea7..00000000000 --- a/vendor/assets/javascripts/jquery.timeago.js +++ /dev/null @@ -1,182 +0,0 @@ -/* eslint-disable */ -/** - * Timeago is a jQuery plugin that makes it easy to support automatically - * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). - * - * @name timeago - * @version 1.1.0 - * @requires jQuery v1.2.3+ - * @author Ryan McGeary - * @license MIT License - http://www.opensource.org/licenses/mit-license.php - * - * For usage and examples, visit: - * http://timeago.yarp.com/ - * - * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) - */ - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else { - // Browser globals - factory(jQuery); - } -}(function ($) { - $.timeago = function(timestamp) { - if (timestamp instanceof Date) { - return inWords(timestamp); - } else if (typeof timestamp === "string") { - return inWords($.timeago.parse(timestamp)); - } else if (typeof timestamp === "number") { - return inWords(new Date(timestamp)); - } else { - return inWords($.timeago.datetime(timestamp)); - } - }; - var $t = $.timeago; - - $.extend($.timeago, { - settings: { - refreshMillis: 60000, - allowFuture: false, - strings: { - prefixAgo: null, - prefixFromNow: null, - suffixAgo: "ago", - suffixFromNow: "from now", - seconds: "less than a minute", - minute: "about a minute", - minutes: "%d minutes", - hour: "about an hour", - hours: "about %d hours", - day: "a day", - days: "%d days", - month: "about a month", - months: "%d months", - year: "about a year", - years: "%d years", - wordSeparator: " ", - numbers: [] - } - }, - inWords: function(distanceMillis) { - var $l = this.settings.strings; - var prefix = $l.prefixAgo; - var suffix = $l.suffixAgo; - if (this.settings.allowFuture) { - if (distanceMillis < 0) { - prefix = $l.prefixFromNow; - suffix = $l.suffixFromNow; - } - } - - var seconds = Math.abs(distanceMillis) / 1000; - var minutes = seconds / 60; - var hours = minutes / 60; - var days = hours / 24; - var years = days / 365; - - function substitute(stringOrFunction, number) { - var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; - var value = ($l.numbers && $l.numbers[number]) || number; - return string.replace(/%d/i, value); - } - - var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || - seconds < 90 && substitute($l.minute, 1) || - minutes < 45 && substitute($l.minutes, Math.round(minutes)) || - minutes < 90 && substitute($l.hour, 1) || - hours < 24 && substitute($l.hours, Math.round(hours)) || - hours < 42 && substitute($l.day, 1) || - days < 30 && substitute($l.days, Math.round(days)) || - days < 45 && substitute($l.month, 1) || - days < 365 && substitute($l.months, Math.round(days / 30)) || - years < 1.5 && substitute($l.year, 1) || - substitute($l.years, Math.round(years)); - - var separator = $l.wordSeparator || ""; - if ($l.wordSeparator === undefined) { separator = " "; } - return $.trim([prefix, words, suffix].join(separator)); - }, - parse: function(iso8601) { - var s = $.trim(iso8601); - s = s.replace(/\.\d+/,""); // remove milliseconds - s = s.replace(/-/,"/").replace(/-/,"/"); - s = s.replace(/T/," ").replace(/Z/," UTC"); - s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 - return new Date(s); - }, - datetime: function(elem) { - var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); - return $t.parse(iso8601); - }, - isTime: function(elem) { - // jQuery's `is()` doesn't play well with HTML5 in IE - return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); - } - }); - - // functions that can be called via $(el).timeago('action') - // init is default when no action is given - // functions are called with context of a single element - var functions = { - init: function(){ - var refresh_el = $.proxy(refresh, this); - refresh_el(); - var $s = $t.settings; - if ($s.refreshMillis > 0) { - setInterval(refresh_el, $s.refreshMillis); - } - }, - update: function(time){ - $(this).data('timeago', { datetime: $t.parse(time) }); - refresh.apply(this); - } - }; - - $.fn.timeago = function(action, options) { - var fn = action ? functions[action] : functions.init; - if(!fn){ - throw new Error("Unknown function name '"+ action +"' for timeago"); - } - // each over objects here and call the requested function - this.each(function(){ - fn.call(this, options); - }); - return this; - }; - - function refresh() { - var data = prepareData(this); - if (!isNaN(data.datetime)) { - $(this).text(inWords(data.datetime)); - } - return this; - } - - function prepareData(element) { - element = $(element); - if (!element.data("timeago")) { - element.data("timeago", { datetime: $t.datetime(element) }); - var text = $.trim(element.text()); - if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { - element.attr("title", text); - } - } - return element.data("timeago"); - } - - function inWords(date) { - return $t.inWords(distance(date)); - } - - function distance(date) { - return (new Date().getTime() - date.getTime()); - } - - // fix for IE6 suckage - document.createElement("abbr"); - document.createElement("time"); -}));