diff options
341 files changed, 5923 insertions, 3561 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 38b71d74fea..fa1370ea1f3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,6 +23,7 @@ AllCops: - 'tmp/**/*' - 'bin/**/*' - 'generator_templates/**/*' + - 'builds/**/*' # Gems in consecutive lines should be alphabetically sorted Bundler/OrderedGems: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5b101ad6b..f279a57105c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.17.1 (2017-02-28) + +- Replace setInterval with setTimeout to prevent highly frequent requests. !9271 (Takuya Noguchi) +- Disable unused tags count cache for Projects, Builds and Runners. +- Spam check and reCAPTCHA improvements. +- Allow searching issues for strings containing colons. +- Disabled tooltip on add issues button in usse boards. +- Fixed commit search UI. +- Fix MR changes tab size count when there are over 100 files in the diff. +- Disable invalid service templates. +- Use default branch as target_branch when parameter is missing. +- Upgrade GitLab Pages to v0.3.2. +- Add performance query regression fix for !9088 affecting #27267. +- Chat slash commands show labels correctly. + ## 8.17.0 (2017-02-22) - API: Fix file downloading. !0 (8267) @@ -182,6 +197,12 @@ entry. - Remove deprecated GitlabCiService. - Requeue pending deletion projects. +## 8.16.7 (2017-02-27) + +- No changes. +- No changes. +- Fix MR changes tab size count when there are over 100 files in the diff. + ## 8.16.6 (2017-02-17) - API: Fix file downloading. !0 (8267) @@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0' # Authentication libraries gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' -gem 'omniauth', '~> 1.3.2' +gem 'omniauth', '~> 1.4.2' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-cas3', '~> 1.1.2' @@ -68,7 +68,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.18.0' +gem 'grape', '~> 0.19.0' gem 'grape-entity', '~> 0.6.0' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index b46b09c7544..472cee510cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -304,7 +304,7 @@ GEM multi_json (~> 1.11) os (~> 0.9) signet (~> 0.7) - grape (0.18.0) + grape (0.19.1) activesupport builder hashie (>= 2.1.0) @@ -328,7 +328,7 @@ GEM temple (~> 0.7.6) thor tilt - hashie (3.4.4) + hashie (3.5.5) health_check (2.2.1) rails (>= 4.0) hipchat (1.5.2) @@ -353,8 +353,8 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.2) - i18n (0.8.0) - ice_nine (0.11.1) + i18n (0.8.1) + ice_nine (0.11.2) influxdb (0.2.3) cause json @@ -417,7 +417,7 @@ GEM minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.12.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) mustermann (0.4.0) tool (~> 0.2) @@ -441,7 +441,7 @@ GEM octokit (4.6.2) sawyer (~> 0.8.0, >= 0.5.3) oj (2.17.4) - omniauth (1.3.2) + omniauth (1.4.2) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) @@ -758,7 +758,7 @@ GEM eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (0.19.4) - thread_safe (0.3.5) + thread_safe (0.3.6) tilt (2.0.6) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -886,7 +886,7 @@ DEPENDENCIES gollum-rugged_adapter (~> 0.4.2) gon (~> 6.1.0) google-api-client (~> 0.8.6) - grape (~> 0.18.0) + grape (~> 0.19.0) grape-entity (~> 0.6.0) haml_lint (~> 0.21.0) hamlit (~> 2.6.1) @@ -920,7 +920,7 @@ DEPENDENCIES oauth2 (~> 1.2.0) octokit (~> 4.6.2) oj (~> 2.17.4) - omniauth (~> 1.3.2) + omniauth (~> 1.4.2) omniauth-auth0 (~> 1.4.1) omniauth-authentiq (~> 0.3.0) omniauth-azure-oauth2 (~> 0.0.6) @@ -1011,4 +1011,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.14.3 + 1.14.4 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8e468faedbf..c51860d1604 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -6,13 +6,7 @@ /* global AwardsHandler */ /* global Aside */ -function requireAll(context) { return context.keys().map(context); } - window.$ = window.jQuery = require('jquery'); -require('jquery-ui/ui/autocomplete'); -require('jquery-ui/ui/draggable'); -require('jquery-ui/ui/effect-highlight'); -require('jquery-ui/ui/sortable'); require('jquery-ujs'); require('vendor/jquery.endless-scroll'); require('vendor/jquery.highlight'); @@ -46,15 +40,176 @@ require('./shortcuts_dashboard_navigation'); require('./shortcuts_issuable'); require('./shortcuts_network'); require('vendor/jquery.nicescroll'); -requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); + +// behaviors +require('./behaviors/autosize'); +require('./behaviors/details_behavior'); +require('./behaviors/quick_submit'); +require('./behaviors/requires_input'); +require('./behaviors/toggler_behavior'); + +// blob +require('./blob/blob_ci_yaml'); +require('./blob/blob_dockerfile_selector'); +require('./blob/blob_dockerfile_selectors'); +require('./blob/blob_file_dropzone'); +require('./blob/blob_gitignore_selector'); +require('./blob/blob_gitignore_selectors'); +require('./blob/blob_license_selector'); +require('./blob/blob_license_selectors'); +require('./blob/template_selector'); + +// templates +require('./templates/issuable_template_selector'); +require('./templates/issuable_template_selectors'); + +// commit +require('./commit/file.js'); +require('./commit/image_file.js'); + +// extensions +require('./extensions/array'); +require('./extensions/custom_event'); +require('./extensions/element'); +require('./extensions/jquery'); +require('./extensions/object'); + +// lib/utils +require('./lib/utils/animate'); +require('./lib/utils/bootstrap_linked_tabs'); +require('./lib/utils/common_utils'); +require('./lib/utils/datetime_utility'); +require('./lib/utils/notify'); +require('./lib/utils/pretty_time'); +require('./lib/utils/text_utility'); +require('./lib/utils/type_utility'); +require('./lib/utils/url_utility'); + +// u2f +require('./u2f/authenticate'); +require('./u2f/error'); +require('./u2f/register'); +require('./u2f/util'); + +// droplab +require('./droplab/droplab'); +require('./droplab/droplab_ajax'); +require('./droplab/droplab_ajax_filter'); +require('./droplab/droplab_filter'); + +// everything else +require('./abuse_reports'); +require('./activities'); +require('./admin'); +require('./ajax_loading_spinner'); +require('./api'); +require('./aside'); +require('./autosave'); +require('./awards_handler'); +require('./breakpoints'); +require('./broadcast_message'); +require('./build'); +require('./build_artifacts'); +require('./build_variables'); +require('./ci_lint_editor'); +require('./commit'); +require('./commits'); +require('./compare'); +require('./compare_autocomplete'); +require('./confirm_danger_modal'); +require('./copy_as_gfm'); +require('./copy_to_clipboard'); +require('./create_label'); +require('./diff'); +require('./dispatcher'); +require('./dropzone_input'); +require('./due_date_select'); +require('./files_comment_button'); +require('./flash'); +require('./gfm_auto_complete'); +require('./gl_dropdown'); +require('./gl_field_error'); +require('./gl_field_errors'); +require('./gl_form'); +require('./group_avatar'); +require('./group_label_subscription'); +require('./groups_select'); +require('./header'); +require('./importer_status'); +require('./issuable'); +require('./issuable_context'); +require('./issuable_form'); +require('./issue'); +require('./issue_status_select'); +require('./issues_bulk_assignment'); +require('./label_manager'); +require('./labels'); +require('./labels_select'); +require('./layout_nav'); +require('./line_highlighter'); +require('./logo'); +require('./member_expiration_date'); +require('./members'); +require('./merge_request'); +require('./merge_request_tabs'); +require('./merge_request_widget'); +require('./merged_buttons'); +require('./milestone'); +require('./milestone_select'); +require('./mini_pipeline_graph_dropdown'); +require('./namespace_select'); +require('./new_branch_form'); +require('./new_commit_form'); +require('./notes'); +require('./notifications_dropdown'); +require('./notifications_form'); +require('./pager'); +require('./pipelines'); +require('./preview_markdown'); +require('./project'); +require('./project_avatar'); +require('./project_find_file'); +require('./project_fork'); +require('./project_import'); +require('./project_label_subscription'); +require('./project_new'); +require('./project_select'); +require('./project_show'); +require('./project_variables'); +require('./projects_list'); +require('./render_gfm'); +require('./render_math'); +require('./right_sidebar'); +require('./search'); +require('./search_autocomplete'); +require('./shortcuts'); +require('./shortcuts_blob'); +require('./shortcuts_dashboard_navigation'); +require('./shortcuts_find_file'); +require('./shortcuts_issuable'); +require('./shortcuts_navigation'); +require('./shortcuts_network'); +require('./signin_tabs_memoizer'); +require('./single_file_diff'); +require('./smart_interval'); +require('./snippets_list'); +require('./star'); +require('./subbable_resource'); +require('./subscription'); +require('./subscription_select'); +require('./syntax_highlight'); +require('./task_list'); +require('./todos'); +require('./tree'); +require('./user'); +require('./user_tabs'); +require('./username_validator'); +require('./users_select'); +require('./version_check_image'); +require('./visibility_select'); +require('./wikis'); +require('./zen_mode'); + require('vendor/fuzzaldrin-plus'); require('es6-promise').polyfill(); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 5152be56b66..8158ed4ec2c 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -123,14 +123,18 @@ class List { if (listFrom) { this.issuesSize += 1; - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) - .then(() => { - listFrom.getIssues(false); - }); + this.updateIssueLabel(issue, listFrom); } } } + updateIssueLabel(issue, listFrom) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + .then(() => { + listFrom.getIssues(false); + }); + } + findIssue (id) { return this.issues.filter(issue => issue.id === id)[0]; } diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 50842ecbaaa..56436c8fdc7 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -92,9 +92,12 @@ const issueLists = issue.getLists(); const listLabels = issueLists.map(listIssue => listIssue.label); - // Add to new lists issues if it doesn't already exist if (!issueTo) { + // Add to new lists issues if it doesn't already exist listTo.addIssue(issue, listFrom, newIndex); + } else { + listTo.updateIssueLabel(issue, listFrom); + issueTo.removeLabel(listFrom.label); } if (listTo.type === 'done') { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 8fa1aceddff..6e6e9b18686 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -7,7 +7,7 @@ var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { - Build.interval = null; + Build.timeout = null; Build.state = null; @@ -31,7 +31,7 @@ this.$scrollBottomBtn = $('#scroll-bottom'); this.$buildRefreshAnimation = $('.js-build-refresh'); - clearInterval(Build.interval); + clearTimeout(Build.timeout); // Init breakpoint checker this.bp = Breakpoints.get(); @@ -52,17 +52,7 @@ this.getInitialBuildTrace(); this.initScrollButtonAffix(); } - if (this.buildStatus === "running" || this.buildStatus === "pending") { - Build.interval = setInterval((function(_this) { - // Check for new build output if user still watching build page - // Only valid for runnig build when output changes during time - return function() { - if (_this.location() === _this.pageUrl) { - return _this.getBuildTrace(); - } - }; - })(this), 4000); - } + this.invokeBuildTrace(); } Build.prototype.initSidebar = function() { @@ -75,6 +65,22 @@ return window.location.href.split("#")[0]; }; + Build.prototype.invokeBuildTrace = function() { + var continueRefreshStatuses = ['running', 'pending']; + // Continue to update build trace when build is running or pending + if (continueRefreshStatuses.indexOf(this.buildStatus) !== -1) { + // Check for new build output if user still watching build page + // Only valid for runnig build when output changes during time + Build.timeout = setTimeout((function(_this) { + return function() { + if (_this.location() === _this.pageUrl) { + return _this.getBuildTrace(); + } + }; + })(this), 4000); + } + }; + Build.prototype.getInitialBuildTrace = function() { var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; @@ -86,7 +92,7 @@ if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } - if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { + if (removeRefreshStatuses.indexOf(buildData.status) !== -1) { this.$buildRefreshAnimation.remove(); return this.initScrollMonitor(); } @@ -105,6 +111,7 @@ if (log.state) { _this.state = log.state; } + _this.invokeBuildTrace(); if (log.status === "running") { if (log.append) { $('.js-build-output').append(log.html); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 49bb64a3472..17d14dc1e79 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -52,6 +52,30 @@ return this.views[viewMode].call(this); }; + ImageFile.prototype.initDraggable = function($el, padding, callback) { + var dragging = false; + var $body = $('body'); + var $offsetEl = $el.parent(); + + $el.off('mousedown').on('mousedown', function() { + dragging = true; + $body.css('user-select', 'none'); + }); + + $body.off('mouseup').off('mousemove').on('mouseup', function() { + dragging = false; + $body.css('user-select', ''); + }) + .on('mousemove', function(e) { + var left; + if (!dragging) return; + + left = e.pageX - ($offsetEl.offset().left + padding); + + callback(e, left); + }); + }; + prepareFrames = function(view) { var maxHeight, maxWidth; maxWidth = 0; @@ -96,26 +120,30 @@ maxHeight = 0; return $('.swipe.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.swipe-frame', view).css({ + $swipeFrame = $('.swipe-frame', view); + $swipeWrap = $('.swipe-wrap', view); + $swipeBar = $('.swipe-bar', view); + + $swipeFrame.css({ width: maxWidth + 16, height: maxHeight + 28 }); - $('.swipe-wrap', view).css({ + $swipeWrap.css({ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.swipe-bar', view).css({ + $swipeBar.css({ left: 0 - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); - }, - stop: function(event) { - return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left); + }); + + wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10); + + _this.initDraggable($swipeBar, wrapPadding, function(e, left) { + if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) { + $swipeWrap.width((maxWidth + 1) - left); + $swipeBar.css('left', left); } }); }; @@ -128,9 +156,14 @@ dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width(); return $('.onion-skin.view', this.file).each((function(_this) { return function(index, view) { - var ref; + var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; - $('.onion-skin-frame', view).css({ + $frame = $('.onion-skin-frame', view); + $frameAdded = $('.frame.added', view); + $track = $('.drag-track', view); + $dragger = $('.dragger', $track); + + $frame.css({ width: maxWidth + 16, height: maxHeight + 28 }); @@ -138,16 +171,18 @@ width: maxWidth + 1, height: maxHeight + 2 }); - return $('.dragger', view).css({ + $dragger.css({ left: dragTrackWidth - }).draggable({ - axis: 'x', - containment: 'parent', - drag: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); - }, - stop: function(event) { - return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth); + }); + + framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); + + _this.initDraggable($dragger, framePadding, function(e, left) { + var opacity = left / dragTrackWidth; + + if (opacity >= 0 && opacity <= 1) { + $dragger.css('left', left); + $frameAdded.css('opacity', opacity); } }); }; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index 3efeb141008..7ae9de7297c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -75,8 +75,11 @@ const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); eventItem.totalTime = eventItem.total_time; - eventItem.author.webUrl = eventItem.author.web_url; - eventItem.author.avatarUrl = eventItem.author.avatar_url; + + if (eventItem.author) { + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; + } if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 089ecedeb78..0f678492d4c 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -36,6 +36,7 @@ /* global Shortcuts */ const ShortcutsBlob = require('./shortcuts_blob'); +const UserCallout = require('./user_callout'); (function() { var Dispatcher; @@ -277,6 +278,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'ci:lints:show': new gl.CILintEditor(); break; + case 'users:show': + new UserCallout(); + break; } switch (path.first()) { case 'sessions': @@ -313,6 +317,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'dashboard': case 'root': shortcut_handler = new ShortcutsDashboardNavigation(); + new UserCallout(); break; case 'profiles': new NotificationsForm(); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 4b700a39d44..5869323d1e2 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -145,7 +145,7 @@ module.exports = Vue.component('environment-component', { </div> </div> - <div class="environments-container"> + <div class="content-list environments-container"> <div class="environments-list-loading text-center" v-if="isLoading"> <i class="fa fa-spinner fa-spin"></i> </div> @@ -181,12 +181,12 @@ module.exports = Vue.component('environment-component', { :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"> </environment-table> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> </div> + + <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"> + </table-pagination> </div> </div> `, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index ad9d1d21a79..3f782742c56 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -503,32 +503,30 @@ module.exports = Vue.component('environment-item', { </span> </td> - <td class="hidden-xs"> - <div v-if="!model.isFolder"> - <div class="btn-group" role="group"> - <actions-component v-if="hasManualActions && canCreateDeployment" - :play-icon-svg="playIconSvg" - :actions="manualActions"> - </actions-component> - - <external-url-component v-if="externalURL && canReadEnvironment" - :external-url="externalURL"> - </external-url-component> - - <stop-component v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path"> - </stop-component> - - <terminal-button-component v-if="model && model.terminal_path" - :terminal-icon-svg="terminalIconSvg" - :terminal-path="model.terminal_path"> - </terminal-button-component> - - <rollback-component v-if="canRetry && canCreateDeployment" - :is-last-deployment="isLastDeployment" - :retry-url="retryUrl"> - </rollback-component> - </div> + <td class="environments-actions"> + <div v-if="!model.isFolder" class="btn-group pull-right" role="group"> + <actions-component v-if="hasManualActions && canCreateDeployment" + :play-icon-svg="playIconSvg" + :actions="manualActions"> + </actions-component> + + <external-url-component v-if="externalURL && canReadEnvironment" + :external-url="externalURL"> + </external-url-component> + + <stop-component v-if="hasStopAction && canCreateDeployment" + :stop-url="model.stop_path"> + </stop-component> + + <terminal-button-component v-if="model && model.terminal_path" + :terminal-icon-svg="terminalIconSvg" + :terminal-path="model.terminal_path"> + </terminal-button-component> + + <rollback-component v-if="canRetry && canCreateDeployment" + :is-last-deployment="isLastDeployment" + :retry-url="retryUrl"> + </rollback-component> </div> </td> </tr> diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 index fd35d77fd3d..33ebca19f5d 100644 --- a/app/assets/javascripts/environments/components/environments_table.js.es6 +++ b/app/assets/javascripts/environments/components/environments_table.js.es6 @@ -46,7 +46,7 @@ module.exports = Vue.component('environment-table-component', { }, template: ` - <table class="table ci-table environments"> + <table class="table ci-table"> <thead> <tr> <th class="environments-name">Environment</th> @@ -54,7 +54,7 @@ module.exports = Vue.component('environment-table-component', { <th class="environments-build">Job</th> <th class="environments-commit">Commit</th> <th class="environments-date">Updated</th> - <th class="hidden-xs environments-actions"></th> + <th class="environments-actions"></th> </tr> </thead> <tbody> diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 698870d0ce1..6d86888dcb8 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,16 +1,16 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* global FilesCommentButton */ +/* global notes */ (function() { + let $commentButtonTemplate; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; + var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; COMMENT_BUTTON_CLASS = '.add-diff-note'; - COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); - LINE_HOLDER_CLASS = '.line_holder'; LINE_NUMBER_CLASS = 'diff-line-num'; @@ -27,26 +27,29 @@ TEXT_FILE_SELECTOR = '.text-file'; - DEBOUNCE_TIMEOUT_DURATION = 100; - function FilesCommentButton(filesContainerElement) { - var debounce; - this.filesContainerElement = filesContainerElement; - this.destroy = bind(this.destroy, this); this.render = bind(this.render, this); - this.VIEW_TYPE = $('input#view[type=hidden]').val(); - debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); - $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); + this.hideButton = bind(this.hideButton, this); + this.isParallelView = notes.isParallelView(); + filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) + .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); } FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement; + var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; $currentTarget = $(e.currentTarget); - - buttonParentElement = this.getButtonParent($currentTarget); - if (!this.validateButtonParent(buttonParentElement)) return; lineContentElement = this.getLineContent($currentTarget); - if (!this.validateLineContent(lineContentElement)) return; + buttonParentElement = this.getButtonParent($currentTarget); + + if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; + + $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); + buttonParentElement.addClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); + + if ($button.length) { + return; + } textFileElement = this.getTextFileElement($currentTarget); buttonParentElement.append(this.buildButton({ @@ -61,19 +64,16 @@ })); }; - FilesCommentButton.prototype.destroy = function(e) { - if (this.isMovingToSameType(e)) { - return; - } - $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); + FilesCommentButton.prototype.hideButton = function(e) { + var $currentTarget = $(e.currentTarget); + var buttonParentElement = this.getButtonParent($currentTarget); + + buttonParentElement.removeClass('is-over') + .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); }; FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - var initializedButtonTemplate; - initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({ - COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1) - }); - return $(initializedButtonTemplate).attr({ + return $commentButtonTemplate.clone().attr({ 'data-noteable-type': buttonAttributes.noteableType, 'data-noteable-id': buttonAttributes.noteableID, 'data-commit-id': buttonAttributes.commitID, @@ -86,14 +86,14 @@ }; FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); + return hoveredElement.closest(TEXT_FILE_SELECTOR); }; FilesCommentButton.prototype.getLineContent = function(hoveredElement) { if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { return hoveredElement; } - if (this.VIEW_TYPE === 'inline') { + if (!this.isParallelView) { return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); } else { return $(hoveredElement).next("." + LINE_CONTENT_CLASS); @@ -101,7 +101,7 @@ }; FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (this.VIEW_TYPE === 'inline') { + if (!this.isParallelView) { if (hoveredElement.hasClass(OLD_LINE_CLASS)) { return hoveredElement; } @@ -114,17 +114,8 @@ } }; - FilesCommentButton.prototype.isMovingToSameType = function(e) { - var newButtonParent; - newButtonParent = this.getButtonParent($(e.toElement)); - if (!newButtonParent) { - return false; - } - return newButtonParent.is(this.getButtonParent($(e.currentTarget))); - }; - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; + return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); }; FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { @@ -135,6 +126,8 @@ })(); $.fn.filesCommentButton = function() { + $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + if (!(this && (this.parent().data('can-create-note') != null))) { return; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 index fbc72a3001a..dd565da507e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -48,7 +48,11 @@ } setOffset(offset = 0) { - this.dropdown.style.left = `${offset}px`; + if (window.innerWidth > 480) { + this.dropdown.style.left = `${offset}px`; + } else { + this.dropdown.style.left = '0px'; + } } renderContent(forceShowList = false) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index a01662e2f9e..9e6ed06054b 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -63,7 +63,7 @@ } GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) >= 0; + return BLUR_KEYCODES.indexOf(keyCode) !== -1; }; GitLabDropdownFilter.prototype.filter = function(search_text) { @@ -605,7 +605,7 @@ var occurrences; occurrences = fuzzaldrinPlus.match(text, term); return text.split('').map(function(character, i) { - if (indexOf.call(occurrences, i) >= 0) { + if (indexOf.call(occurrences, i) !== -1) { return "<b>" + character + "</b>"; } else { return character; @@ -748,7 +748,7 @@ return function(e) { var $listItems, PREV_INDEX, currentKeyCode; currentKeyCode = e.which; - if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) { + if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { e.preventDefault(); e.stopImmediatePropagation(); PREV_INDEX = currentIndex; diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 8df86f68218..3bfce32768a 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -116,7 +116,7 @@ formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; - issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); + issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&'); issuesUrl += formData; return gl.utils.visitUrl(issuesUrl); }; diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 45a1d90a9d9..0242350f718 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -296,5 +296,58 @@ * @returns {Boolean} */ w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; + + /** + * Back Off exponential algorithm + * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> + * + * @param {Function<next, stop>} fn function to be called + * @param {Number} timeout + * @return {Promise<Any, Error>} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ + w.gl.utils.backOff = (fn, timeout = 60000) => { + const maxInterval = 32000; + let nextInterval = 2000; + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (Date.now() - startTime < timeout) { + setTimeout(fn.bind(null, next, stop), nextInterval); + nextInterval = Math.min(nextInterval + nextInterval, maxInterval); + } else { + reject(new Error('BACKOFF_TIMEOUT')); + } + }; + + fn(next, stop); + }); + }; })(window); }).call(window); diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js new file mode 100644 index 00000000000..bc109a69c20 --- /dev/null +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -0,0 +1,10 @@ +/** + * exports HTTP status codes + */ + +const statusCodes = { + NO_CONTENT: 204, + OK: 200, +}; + +module.exports = statusCodes; diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 88f08bbaa34..00c6c050612 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -83,7 +83,7 @@ require('./smart_interval'); return function() { var page; page = $('body').data('page').split(':').last(); - if (allowedPages.indexOf(page) < 0) { + if (allowedPages.indexOf(page) === -1) { return _this.clearEventListeners(); } }; @@ -233,7 +233,7 @@ require('./smart_interval'); } $('.ci_widget').hide(); allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; - if (indexOf.call(allowed_states, state) >= 0) { + if (indexOf.call(allowed_states, state) !== -1) { $('.ci_widget.ci-' + state).show(); switch (state) { case "failed": diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 7fbaeec7882..38c673e8907 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -78,7 +78,6 @@ } else { $(element).find('.assignee-icon').empty(); } - return $(element).effect('highlight'); }; function Milestone() { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index cb24f212c66..5828f460a23 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -20,15 +20,35 @@ }; NewBranchForm.prototype.init = function() { - if (this.name.val().length > 0) { + if (this.name.length && this.name.val().length > 0) { return this.name.trigger('blur'); } }; NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { - return this.ref.autocomplete({ - source: availableRefs, - minLength: 1 + var $branchSelect = $('.js-branch-select'); + + $branchSelect.glDropdown({ + data: availableRefs, + filterable: true, + filterByText: true, + remote: false, + fieldName: $branchSelect.data('field-name'), + selectable: true, + isSelectable: function(branch, $el) { + return !$el.hasClass('is-active'); + }, + text: function(branch) { + return branch; + }, + id: function(branch) { + return branch; + }, + toggleLabel: function(branch) { + if (branch) { + return branch; + } + } }); }; @@ -61,7 +81,7 @@ var errorMessage, errors, formatter, unique, validator; this.branchNameError.empty(); unique = function(values, value) { - if (indexOf.call(values, value) < 0) { + if (indexOf.call(values, value) === -1) { values.push(value); } return values; diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index 81374296522..4ccea0624ee 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -84,13 +84,14 @@ } $(function() { - $(document).on('focusout.ssh_key', '#key_key', function() { + $(document).on('input.ssh_key', '#key_key', function() { const $title = $('#key_title'); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); - if (comment && comment.length > 1 && $title.val() === '') { + + // Extract the SSH Key title from its comment + if (comment && comment.length > 1) { return $title.val(comment[1]).change(); } - // Extract the SSH Key title from its comment }); if (global.utils.getPagePath() === 'profiles') { return new Profile(); diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index d7f3c9fd37e..15d32825583 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,3 +1,2 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); +require('./gl_crop'); +require('./profile'); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7c03c8b72d4..db7ceaa2421 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -116,7 +116,7 @@ if ($('input[name="ref"]').length) { var $form = $dropdown.closest('form'); var action = $form.attr('action'); - var divider = action.indexOf('?') < 0 ? '?' : '&'; + var divider = action.indexOf('?') === -1 ? '?' : '&'; gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index 149e511451e..6ef59e94384 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -36,6 +36,9 @@ // Do not update if one dropdown has not selected any option if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + this.$allowedToMergeDropdown.disable(); + this.$allowedToPushDropdown.disable(); + $.ajax({ type: 'POST', url: this.$wrap.data('url'), @@ -53,13 +56,13 @@ }] } }, - success: () => { - this.$wrap.effect('highlight'); - }, error() { $.scrollTo(0); new Flash('Failed to update branch!'); } + }).always(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); }); } }; diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index ffb66caf5f4..849c1e31623 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,3 +1,5 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); +require('./protected_branch_access_dropdown'); +require('./protected_branch_create'); +require('./protected_branch_dropdown'); +require('./protected_branch_edit'); +require('./protected_branch_edit_list'); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 89822246bb8..a98403f4cf2 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,10 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); - (function() { $(function() { var editor = ace.edit("editor"); diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index dfe24d1fb33..b1402c0a880 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,3 +1,4 @@ +/* global Flash */ require('vendor/task_list'); class TaskList { @@ -6,6 +7,16 @@ class TaskList { this.dataType = options.dataType; this.fieldName = options.fieldName; this.onSuccess = options.onSuccess || (() => {}); + this.onError = function showFlash(response) { + let errorMessages = ''; + + if (response.responseJSON) { + errorMessages = response.responseJSON.errors.join(' '); + } + + return new Flash(errorMessages || 'Update failed', 'alert'); + }; + this.init(); } @@ -32,6 +43,7 @@ class TaskList { url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), data: patchData, success: this.onSuccess, + error: this.onError, }); } } diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js new file mode 100644 index 00000000000..74b869502a4 --- /dev/null +++ b/app/assets/javascripts/user_callout.js @@ -0,0 +1,58 @@ +/* global Cookies */ + +const userCalloutElementName = '.user-callout'; +const closeButton = '.close-user-callout'; +const userCalloutBtn = '.user-callout-btn'; +const userCalloutSvgAttrName = 'callout-svg'; + +const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; + +const USER_CALLOUT_TEMPLATE = ` + <div class="bordered-box landing content-block"> + <button class="btn btn-default close close-user-callout" type="button"> + <i class="fa fa-times dismiss-icon"></i> + </button> + <div class="row"> + <div class="col-sm-3 col-xs-12 svg-container"> + </div> + <div class="col-sm-8 col-xs-12 inner-content"> + <h4> + Customize your experience + </h4> + <p> + Change syntax themes, default project pages, and more in preferences. + </p> + <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a> + </div> + </div> +</div>`; + +class UserCallout { + constructor() { + this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); + this.userCalloutBody = $(userCalloutElementName); + this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); + $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); + this.init(); + } + + init() { + const $template = $(USER_CALLOUT_TEMPLATE); + if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { + $template.find('.svg-container').append(this.userCalloutSvg); + this.userCalloutBody.append($template); + $template.find(closeButton).on('click', e => this.dismissCallout(e)); + $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); + } + } + + dismissCallout(e) { + Cookies.set(USER_CALLOUT_COOKIE, 'true'); + const $currentTarget = $(e.currentTarget); + if ($currentTarget.hasClass('close-user-callout')) { + this.userCalloutBody.empty(); + } + } +} + +module.exports = UserCallout; diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 4cad60a59b1..580e2d84be5 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,3 +1 @@ -// require everything else in this directory -function requireAll(context) { return context.keys().map(context); } -requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); +require('./calendar'); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 54e8f977a47..b50afe7c594 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -1,5 +1,5 @@ /* global Vue, Flash, gl */ -/* eslint-disable no-param-reassign */ +/* eslint-disable no-param-reassign, no-alert */ ((gl) => { gl.VuePipelineActions = Vue.extend({ @@ -16,21 +16,33 @@ download(name) { return `Download ${name} artifacts`; }, + + /** + * Shows a dialog when the user clicks in the cancel button. + * We need to prevent the default behavior and stop propagation because the + * link relies on UJS. + * + * @param {Event} event + */ + confirmAction(event) { + if (!confirm('Are you sure you want to cancel this pipeline?')) { + event.preventDefault(); + event.stopPropagation(); + } + }, }, template: ` - <td class="pipeline-actions hidden-xs"> - <div class="controls pull-right"> - <div class="btn-group inline"> - <div class="btn-group"> + <td class="pipeline-actions"> + <div class="pull-right"> + <div class="btn-group"> + <div class="btn-group" v-if="actions"> <button - v-if='actions' class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" data-toggle="dropdown" title="Manual job" data-placement="top" - aria-label="Manual job" - > - <span v-html='svgs.iconPlay' aria-hidden="true"></span> + aria-label="Manual job"> + <span v-html="svgs.iconPlay" aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> <ul class="dropdown-menu dropdown-menu-align-right"> @@ -38,23 +50,21 @@ <a rel="nofollow" data-method="post" - :href='action.path' - > - <span v-html='svgs.iconPlay' aria-hidden="true"></span> + :href="action.path"> + <span v-html="svgs.iconPlay" aria-hidden="true"></span> <span>{{action.name}}</span> </a> </li> </ul> </div> - <div class="btn-group"> + + <div class="btn-group" v-if="artifacts"> <button - v-if='artifacts' class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" title="Artifacts" data-placement="top" data-toggle="dropdown" - aria-label="Artifacts" - > + aria-label="Artifacts"> <i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> @@ -62,41 +72,39 @@ <li v-for='artifact in pipeline.details.artifacts'> <a rel="nofollow" - download - :href='artifact.path' - > + :href="artifact.path"> <i class="fa fa-download" aria-hidden="true"></i> <span>{{download(artifact.name)}}</span> </a> </li> </ul> </div> - </div> - <div class="cancel-retry-btns inline"> - <a - v-if='pipeline.flags.retryable' - class="btn has-tooltip" - title="Retry" - rel="nofollow" - data-method="post" - data-placement="top" - data-toggle="dropdown" - :href='pipeline.retry_path' - aria-label="Retry"> - <i class="fa fa-repeat" aria-hidden="true"></i> - </a> - <a - v-if='pipeline.flags.cancelable' - class="btn btn-remove has-tooltip" - title="Cancel" - rel="nofollow" - data-method="post" - data-placement="top" - data-toggle="dropdown" - :href='pipeline.cancel_path' - aria-label="Cancel"> - <i class="fa fa-remove" aria-hidden="true"></i> - </a> + <div class="btn-group" v-if="pipeline.flags.retryable"> + <a + class="btn btn-default btn-retry has-tooltip" + title="Retry" + rel="nofollow" + data-method="post" + data-placement="top" + data-toggle="dropdown" + :href='pipeline.retry_path' + aria-label="Retry"> + <i class="fa fa-repeat" aria-hidden="true"></i> + </a> + </div> + <div class="btn-group" v-if="pipeline.flags.cancelable"> + <a + class="btn btn-remove has-tooltip" + title="Cancel" + rel="nofollow" + data-method="post" + data-placement="top" + data-toggle="dropdown" + :href='pipeline.cancel_path' + aria-label="Cancel"> + <i class="fa fa-remove" aria-hidden="true"></i> + </a> + </div> </div> </div> </td> diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 3598da11573..6048fa691dc 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -54,7 +54,7 @@ require('../lib/utils/datetime_utility'); }, }, template: ` - <td> + <td class="pipelines-time-ago"> <p class="duration" v-if='duration'> <span v-html='svgs.iconTimer'></span> {{duration}} @@ -65,8 +65,7 @@ require('../lib/utils/datetime_utility'); data-toggle="tooltip" data-placement="top" data-container="body" - :data-original-title='localTimeFinished' - > + :data-original-title='localTimeFinished'> {{timeStopped.words}} </time> </p> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4bdaef31ee9..34d3bbdd80d 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -44,7 +44,7 @@ require('./pipelines_table_row'); <th class="js-pipeline-commit pipeline-commit">Commit</th> <th class="js-pipeline-stages pipeline-stages">Stages</th> <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> + <th class="js-pipeline-actions pipeline-actions"></th> </tr> </thead> <tbody> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 1dcd1f8a6fc..83a8eeaafde 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -2,7 +2,6 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - *= require jquery-ui/autocomplete *= require jquery.atwho *= require select2 *= require_self diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 0f9213b98e3..9a4129cdc8d 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -229,7 +229,7 @@ .controls { float: right; margin-top: 8px; - padding-bottom: 7px; + padding-bottom: 8px; border-bottom: 1px solid $border-color; } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e3da467a27c..d2be8dc7a39 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -26,6 +26,11 @@ .filtered-search-container { display: -webkit-flex; display: flex; + + @media (max-width: $screen-xs-min) { + -webkit-flex-direction: column; + flex-direction: column; + } } .filtered-search-input-container { @@ -34,6 +39,20 @@ position: relative; width: 100%; + @media (max-width: $screen-xs-min) { + -webkit-flex: 1 1 100%; + flex: 1 1 100%; + margin-bottom: 10px; + + .dropdown-menu { + width: auto; + left: 0; + right: 0; + max-width: none; + min-width: 100%; + } + } + .form-control { padding-left: 25px; padding-right: 25px; @@ -79,6 +98,31 @@ overflow: auto; } +@media (max-width: $screen-xs-min) { + .issues-details-filters { + padding: 0 0 10px; + background-color: $white-light; + border-top: 0; + } + + .filter-dropdown-container { + .dropdown-toggle, + .dropdown { + width: 100%; + } + + .dropdown { + margin-left: 0; + } + + .fa-chevron-down { + position: absolute; + right: 10px; + top: 10px; + } + } +} + %filter-dropdown-item-btn-hover { background-color: $dropdown-hover-color; color: $white-light; @@ -148,4 +192,4 @@ .filter-dropdown-loading { padding: 8px 16px; -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3945a789c82..685a4847731 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -148,16 +148,11 @@ header { } .header-logo { - position: absolute; - left: 50%; + display: inline-block; + margin: 0 8px 0 3px; + position: relative; top: 7px; transition-duration: .3s; - z-index: 999; - - #logo { - position: relative; - left: -50%; - } svg, img { @@ -167,15 +162,6 @@ header { &:hover { cursor: pointer; } - - @media (max-width: $screen-xs-max) { - right: 20px; - left: auto; - - #logo { - left: auto; - } - } } .title { @@ -183,7 +169,6 @@ header { padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -193,14 +178,18 @@ header { vertical-align: top; white-space: nowrap; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - @media (max-width: $screen-xs-max) { max-width: 190px; } + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + max-width: 428px; + } + + @media (min-width: $screen-lg-min) { + max-width: 685px; + } + a { color: $gl-text-color; diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index d335fedefe2..300ba4f2de6 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,17 +2,6 @@ font-family: $regular_font; font-size: $font-size-base; - &.ui-autocomplete { - border-color: $jq-ui-border; - padding: 0; - margin-top: 2px; - z-index: 1001; - - .ui-menu-item a { - padding: 4px 10px; - } - } - .ui-state-default { border: 1px solid $white-light; background: $white-light; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 6f2e746d4b0..9c76e58dfc8 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792; $dark-highlight-color: $black; $dark-pre-hll-bg: #373b41; $dark-hll-bg: #373b41; +$dark-over-bg: #9f9ab5; $dark-c: #969896; $dark-err: #c66; $dark-k: #b294bb; @@ -139,6 +140,18 @@ $dark-il: #de935f; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $dark-over-bg; + border-color: darken($dark-over-bg, 5%); + + a { + color: darken($dark-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 2144a5f7466..6488a099c74 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e; $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-diff-border: #808080; $monokai-highlight-bg: #ffe792; +$monokai-over-bg: #9f9ab5; $monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-idiff: rgba(166, 226, 46, 0.15); @@ -139,6 +140,18 @@ $monokai-gi: #a6e22e; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $monokai-over-bg; + border-color: darken($monokai-over-bg, 5%); + + a { + color: darken($monokai-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 2cb1d18f12f..00079cc2b5b 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c; $solarized-dark-line-color-old: #7a6c71; $solarized-dark-highlight: #094554; $solarized-dark-hll-bg: #174652; +$solarized-dark-over-bg: #9f9ab5; $solarized-dark-c: #586e75; $solarized-dark-err: #93a1a1; $solarized-dark-g: #93a1a1; @@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $solarized-dark-over-bg; + border-color: darken($solarized-dark-over-bg, 5%); + + a { + color: darken($solarized-dark-over-bg, 15%); + } + } + } + .line_content.match { @include dark-diff-match-line; } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index b72c4326730..2e3b68f1a58 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080; $solarized-light-line-color-old: #ad9186; $solarized-light-highlight: #eee8d5; $solarized-light-hll-bg: #ddd8c5; +$solarized-light-over-bg: #ded7fc; $solarized-light-c: #93a1a1; $solarized-light-err: #586e75; $solarized-light-g: #586e75; @@ -150,6 +151,18 @@ $solarized-light-il: #2aa198; } } + .diff-line-num { + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $solarized-light-over-bg; + border-color: darken($solarized-light-over-bg, 5%); + + a { + color: darken($solarized-light-over-bg, 15%); + } + } + } + .line_content.match { @include matchLine; } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 398fbfd3b18..0eca30e570f 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -7,6 +7,7 @@ $white-code-color: $gl-text-color; $white-highlight: #fafe3d; $white-pre-hll-bg: #f8eec7; $white-hll-bg: #f8f8f8; +$white-over-bg: #ded7fc; $white-c: #998; $white-err: #a61717; $white-err-bg: #e3d2d2; @@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5; } } + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $white-over-bg; + border-color: darken($white-over-bg, 5%); + + a { + color: darken($white-over-bg, 15%); + } + } + &.hll:not(.empty-cell) { background-color: $line-number-select; border-color: $line-select-yellow-dark; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 92d7772da57..339cdcde480 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -89,6 +89,10 @@ .diff-line-num { width: 50px; + + a { + transition: none; + } } .line_holder td { @@ -109,10 +113,6 @@ td.line_content.parallel { width: 46%; } - - .add-diff-note { - margin-left: -65px; - } } .old_line, diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index f789ae1ccd3..77e09e66340 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -15,112 +15,97 @@ padding-top: 20px; } -@media (max-width: $screen-xs-max) { - .environments-container { +.environments-container { + .table-holder { width: 100%; overflow: auto; } -} - -.environments { - table-layout: fixed; - - .environments-commit, - .environments-actions, - .environments-deploy, - .environments-build, - .environments-date { - position: static; - float: none; - display: table-cell; - } - - .environments-commit, - .environments-actions { - width: 20%; - } - - .environments-date { - width: 10%; - } - .environments-name, - .environments-deploy, - .environments-build { - width: 15%; - } - - .environment-name, - .environments-build-cell, - .deployment-column { - word-break: break-all; - } - - .deployment-column { - .avatar { - float: none; + .table.ci-table { + .environments-actions { + min-width: 200px; } - } - .btn-group { + .environments-commit, + .environments-actions { + width: 20%; + } - > a { - color: $gl-text-color-secondary; + .environments-date { + width: 10%; } - svg path { - fill: $gl-text-color-secondary; + .environments-name, + .environments-deploy, + .environments-build { + width: 15%; } - .dropdown { - outline: none; + .deployment-column { + > span { + word-break: break-all; + } + + .avatar { + float: none; + } } - } + .btn-group { - .commit-title { - margin: 0; - } + > a { + color: $gl-text-color-secondary; + } - .avatar-image-container { - text-decoration: none; - } + svg path { + fill: $gl-text-color-secondary; + } - .icon-play { - height: 13px; - width: 12px; - } + .dropdown { + outline: none; + } + } - .external-url, - .dropdown-new { - color: $gl-text-color-secondary; - } + .commit-title { + margin: 0; + } - .dropdown-menu { + .avatar-image-container { + text-decoration: none; + } - .fa { - margin-right: 6px; - color: $gl-text-color-secondary; + .icon-play { + height: 13px; + width: 12px; } - } - .build-link, - .branch-name { - color: $gl-text-color; - } + .external-url, + .dropdown-new { + color: $gl-text-color-secondary; + } - .stop-env-link, - .external-url { - color: $gl-text-color-secondary; + .dropdown-menu { + .fa { + margin-right: 6px; + color: $gl-text-color-secondary; + } + } - .stop-env-icon { - font-size: 14px; + .build-link, + .branch-name { + color: $gl-text-color; } - } - .deployment { - .build-column { + .stop-env-link, + .external-url { + color: $gl-text-color-secondary; + + .stop-env-icon { + font-size: 14px; + } + } + .deployment .build-column { .build-link { color: $gl-text-color; } @@ -129,34 +114,32 @@ float: none; } } - } - - .folder-icon { - margin-right: 3px; - color: $gl-text-color-secondary; - display: inline-block; - .fa:nth-child(1) { + .folder-icon { margin-right: 3px; + color: $gl-text-color-secondary; + display: inline-block; + + .fa:nth-child(1) { + margin-right: 3px; + } } - } - .folder-name { - cursor: pointer; - color: $gl-text-color-secondary; - display: inline-block; - } -} + .folder-name { + cursor: pointer; + color: $gl-text-color-secondary; + display: inline-block; + } -.table.ci-table.environments { - .icon-container { - width: 20px; - text-align: center; - } + .icon-container { + width: 20px; + text-align: center; + } - .branch-commit { - .commit-id { - margin-right: 0; + .branch-commit { + .commit-id { + margin-right: 0; + } } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index aa130a1abb0..00f5f2645b3 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -452,36 +452,37 @@ ul.notes { * Line note button on the side of diffs */ -.diff-file tr.line_holder { - @mixin show-add-diff-note { - display: inline-block; - } +.add-diff-note { + display: none; + margin-top: -2px; + border-radius: 50%; + background: $white-light; + padding: 1px 5px; + font-size: 12px; + color: $gl-link-color; + margin-left: -55px; + position: absolute; + z-index: 10; + width: 23px; + height: 23px; + border: 1px solid $border-color; + transition: transform .1s ease-in-out; - .add-diff-note { - margin-top: -8px; - border-radius: 40px; - background: $white-light; - padding: 4px; - font-size: 16px; - color: $gl-link-color; - margin-left: -56px; - position: absolute; - z-index: 10; - width: 32px; - // "hide" it by default - display: none; + &:hover { + background: $gl-info; + color: $white-light; + transform: scale(1.15); + } - &:hover { - background: $gl-info; - color: $white-light; - @include show-add-diff-note; - } + &:active { + outline: 0; } +} - // "show" the icon also if we just hover somewhere over the line - &:hover > td { +.diff-file { + .is-over { .add-diff-note { - @include show-add-diff-note; + display: inline-block; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 3fe1eef307e..69eea1b2217 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -13,21 +13,16 @@ white-space: nowrap; } - .commit-title { - margin: 0; - } - - .controls { - white-space: nowrap; + .table-holder { + width: 100%; + overflow: auto; } - .btn { - margin: 4px; + .commit-title { + margin: 0; } .table.ci-table { - min-width: 1200px; - table-layout: fixed; .label { margin-bottom: 3px; @@ -37,16 +32,72 @@ color: $black; } - .pipeline-date, - .pipeline-status { - width: 10%; + .stage-cell { + min-width: 130px; // Guarantees we show at least 4 stages in line + width: 20%; + } + + .pipelines-time-ago { + text-align: right; } - .pipeline-info, - .pipeline-commit, - .pipeline-stages, .pipeline-actions { - width: 20%; + padding-right: 0; + min-width: 170px; //Guarantees buttons don't break in several lines. + + .btn-default { + color: $gl-text-color-secondary; + } + + .btn.btn-retry:hover, + .btn.btn-retry:focus { + border-color: $gray-darkest; + background-color: $white-normal; + } + + svg path { + fill: $gl-text-color-secondary; + } + + .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } + + .dropdown-toggle, + .dropdown-menu { + color: $gl-text-color-secondary; + + .fa { + color: $gl-text-color-secondary; + font-size: 14px; + } + + svg, + .fa { + margin-right: 0; + } + } + + .btn-group { + &.open { + .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; + } + } + + .btn { + .icon-play { + height: 13px; + width: 12px; + } + } + } + + .tooltip { + white-space: nowrap; + } } } } @@ -54,6 +105,7 @@ @media (max-width: $screen-md-max) { .content-list { &.pipelines, + &.environments-container, &.builds-content-list { width: 100%; overflow: auto; @@ -61,27 +113,10 @@ } } -.content-list.pipelines .table-holder { - min-height: 300px; -} - -.pipeline-holder { - width: 100%; - overflow: auto; -} - .table.ci-table { - min-width: 900px; - - &.pipeline { - min-width: 650px; - } - - &.builds-page { - tr { - height: 71px; - } + &.builds-page tr { + height: 71px; } tr { @@ -94,12 +129,16 @@ padding: 10px 8px; } + td.environments-actions { + padding-right: 0; + } + td.stage-cell { padding: 10px 0; } .commit-link { - padding: 9px 8px 10px; + padding: 9px 8px 10px 2px; } } @@ -206,72 +245,8 @@ } } - .pipeline-actions { - min-width: 140px; - - .btn { - margin: 0; - color: $gl-text-color-secondary; - } - - .cancel-retry-btns { - vertical-align: middle; - - .btn:not(:first-child) { - margin-left: 8px; - } - } - - .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } - - .dropdown-toggle, - .dropdown-menu { - color: $gl-text-color-secondary; - - .fa { - color: $gl-text-color-secondary; - font-size: 14px; - } - - svg, - .fa { - margin-right: 0; - } - } - - .btn-remove { - color: $white-light; - } - - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } - - .btn { - .icon-play { - height: 13px; - width: 12px; - } - } - } - - .tooltip { - white-space: nowrap; - } - } - - .build-link { - - a { - color: $gl-text-color; - } + .build-link a { + color: $gl-text-color; } .btn-group.open .dropdown-toggle { @@ -335,31 +310,8 @@ } .tab-pane { - &.pipelines { - .ci-table { - min-width: 900px; - } - - .content-list.pipelines { - overflow: auto; - } - - .stage { - max-width: 100px; - width: 100px; - } - - .pipeline-actions { - min-width: initial; - } - } - - &.builds { - .ci-table { - tr { - height: 71px; - } - } + &.builds .ci-table tr { + height: 71px; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8031c4467a4..aad1a8986b0 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -277,3 +277,41 @@ table.u2f-registrations { padding-left: 18px; } } + +.user-callout { + margin: 24px auto 0; + + .bordered-box { + border: 1px solid $border-color; + border-radius: $border-radius-default; + } + + .landing { + margin-bottom: $gl-padding; + + .close { + margin-right: 20px; + } + + .dismiss-icon { + float: right; + cursor: pointer; + color: $cycle-analytics-dismiss-icon-color; + } + + .svg-container { + text-align: center; + + svg { + width: 136px; + height: 136px; + } + } + } + + @media(max-width: $screen-xs-max) { + .inner-content { + padding-left: 30px; + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 67110813abb..07b93430442 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -638,14 +638,6 @@ pre.light-well { margin: 0; } -.activity-filter-block { - .controls { - padding-bottom: 7px; - margin-top: 8px; - border-bottom: 1px solid $border-color; - } -} - .commits-search-form { .input-short { min-width: 200px; diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0821974aa93..3ccf2a9ce33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -26,6 +26,23 @@ module IssuableActions private + def render_conflict_response + respond_to do |format| + format.html do + @conflict = true + render :edit + end + + format.json do + render json: { + errors: [ + "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs." + ] + }, status: 409 + end + end + end + def labels @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index e610ccaec96..2992568ae66 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -33,6 +33,7 @@ module ServiceParams :issues_url, :jira_issue_transition_id, :merge_requests_events, + :mock_service_url, :namespace, :new_issue_url, :notify, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ca5e81100da..1151555b8fa 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -134,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def referenced_merge_requests diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d122c7fdcb2..53f30a24312 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -296,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) - if @merge_request.valid? - respond_to do |format| - format.html do - redirect_to([@merge_request.target_project.namespace.becomes(Namespace), - @merge_request.target_project, @merge_request]) - end - format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + respond_to do |format| + format.html do + if @merge_request.valid? + redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request]) + else + render :edit end end - else - render "edit" + + format.json do + render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + end end rescue ActiveRecord::StaleObjectError - @conflict = true - render :edit + render_conflict_response end def remove_wip diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index ff937b5ebd2..5ac3e66bb1f 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -15,4 +15,11 @@ module BuildsHelper log_state: @build.trace_with_state[:state].to_s } end + + def build_failed_issue_options + { + title: "Build Failed ##{@build.id}", + description: namespace_project_build_url(@project.namespace, @project, @build) + } + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 4c7c16d694c..195094730aa 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -34,7 +34,7 @@ module ButtonHelper content_tag (append_link ? :a : :span), protocol, class: klass, - href: (project.http_url_to_repo if append_link), + href: (project.http_url_to_repo(current_user) if append_link), data: { html: true, placement: placement, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 735a355c25a..4befeacc135 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -241,7 +241,7 @@ module ProjectsHelper when 'ssh' project.ssh_url_to_repo else - project.http_url_to_repo + project.http_url_to_repo(current_user) end end diff --git a/app/models/event.rb b/app/models/event.rb index 4b8eac9accf..d7ca8e3c599 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -36,7 +36,7 @@ class Event < ActiveRecord::Base scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(projects) do - where(project_id: projects).recent + where(project_id: projects.pluck(:id)).recent end scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7eb875f1ef5..d6e7ed87555 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,10 +91,6 @@ class MergeRequest < ActiveRecord::Base around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end - - after_transition unchecked: :cannot_be_merged do |merge_request, transition| - TodoService.new.merge_request_became_unmergeable(merge_request) - end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] diff --git a/app/models/project.rb b/app/models/project.rb index aedd5bedcb9..e06fc30dc8a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -869,8 +869,14 @@ class Project < ActiveRecord::Base url_to_repo end - def http_url_to_repo - "#{web_url}.git" + def http_url_to_repo(user = nil) + url = web_url + + if user + url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" } + end + + "#{url}.git" end # Check if current branch name is marked as protected in the system diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 5cb6b0c527d..ac1e9ab2b0b 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -33,8 +33,15 @@ class ProjectGroupLink < ActiveRecord::Base private def different_group - if self.group && self.project && self.project.group == self.group - errors.add(:base, "Project cannot be shared with the project it is in.") + return unless self.group && self.project + + project_group = self.project.group + return unless project_group + + group_ids = project_group.ancestors.map(&:id).push(project_group.id) + + if group_ids.include?(self.group.id) + errors.add(:base, "Project cannot be shared with the group it is in or one of its ancestors.") end end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 4ebc5318da1..c13538e9fea 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService 'This service sends notifications about projects events to Mattermost channels.<br /> To set up this service: <ol> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> - <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> - <li>Paste the webhook <strong>URL</strong> into the field bellow. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li> + <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li> + <li>Paste the webhook <strong>URL</strong> into the field below.</li> + <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li> </ol>' end @@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "town-square" + "Channel handle (e.g. town-square)" end end diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb new file mode 100644 index 00000000000..a8d581a1f67 --- /dev/null +++ b/app/models/project_services/mock_ci_service.rb @@ -0,0 +1,82 @@ +# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service +class MockCiService < CiService + ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze + + prop_accessor :mock_service_url + validates :mock_service_url, presence: true, url: true, if: :activated? + + def title + 'MockCI' + end + + def description + 'Mock an external CI' + end + + def self.to_param + 'mock_ci' + end + + def fields + [ + { type: 'text', + name: 'mock_service_url', + placeholder: 'http://localhost:4004' }, + ] + end + + # Return complete url to build page + # + # Ex. + # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c + # + def build_page(sha, ref) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}"] + + URI.join(*url).to_s + end + + # Return string with build status or :error symbol + # + # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' + # + # + # Ex. + # @service.commit_status('13be4ac', 'master') + # # => 'success' + # + # @service.commit_status('2abe4ac', 'dev') + # # => 'running' + # + # + def commit_status(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + read_commit_status(response) + rescue Errno::ECONNREFUSED + :error + end + + def commit_status_path(sha) + url = [mock_service_url, + "#{project.namespace.path}/#{project.path}/status/#{sha}.json"] + + URI.join(*url).to_s + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'pending' + else + response['status'] + end + + if status.present? && ALLOWED_STATES.include?(status) + status + else + :error + end + end +end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index f77d2d7c60b..da7496573ef 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -13,11 +13,11 @@ class SlackService < ChatNotificationService def help 'This service sends notifications about projects events to Slack channels.<br /> - To setup this service: + To set up this service: <ol> - <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> - <li>Paste the <strong>Webhook URL</strong> into the field below. </li> - <li>Select events below to enable notifications. The channel and username are optional. </li> + <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li> + <li>Paste the <strong>Webhook URL</strong> into the field below.</li> + <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li> </ol>' end @@ -27,14 +27,14 @@ class SlackService < ChatNotificationService def default_fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, - { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' }, ] end def default_channel_placeholder - "#general" + "Channel name (e.g. general)" end end diff --git a/app/models/repository.rb b/app/models/repository.rb index cd2568ad445..0dbf246c3a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -109,9 +109,7 @@ class Repository offset: offset, after: after, before: before, - # --follow doesn't play well with --skip. See: - # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 - follow: false, + follow: path.present?, skip_merges: skip_merges } @@ -746,136 +744,63 @@ class Repository @tags ||= raw_repository.tags end - # rubocop:disable Metrics/ParameterLists - def commit_dir( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - check_tree_entry_for_dir(branch_name, path) - - if start_branch_name - start_project.repository. - check_tree_entry_for_dir(start_branch_name, path) - end + def create_dir(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :create_dir, file_path: path }] - commit_file( - user, - "#{path}/.gitkeep", - '', - message: message, - branch_name: branch_name, - update: false, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def commit_file( - user, path, content, - message:, branch_name:, update: true, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - unless update - error_message = "Filename already exists; update not allowed" + def create_file(user, path, content, **options) + options[:user] = user + options[:actions] = [{ action: :create, file_path: path, content: content }] - if tree_entry_at(branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end + multi_action(**options) + end - if start_branch_name && - start_project.repository.tree_entry_at(start_branch_name, path) - raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) - end - end + def update_file(user, path, content, **options) + previous_path = options.delete(:previous_path) + action = previous_path && previous_path != path ? :move : :update - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :create, - file_path: path, - content: content }]) - end - # rubocop:enable Metrics/ParameterLists + options[:user] = user + options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }] - # rubocop:disable Metrics/ParameterLists - def update_file( - user, path, content, - message:, branch_name:, previous_path:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - action = if previous_path && previous_path != path - :move - else - :update - end - - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: action, - file_path: path, - content: content, - previous_path: previous_path }]) + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists - # rubocop:disable Metrics/ParameterLists - def remove_file( - user, path, - message:, branch_name:, - author_email: nil, author_name: nil, - start_branch_name: nil, start_project: project) - multi_action( - user: user, - message: message, - branch_name: branch_name, - author_email: author_email, - author_name: author_name, - start_branch_name: start_branch_name, - start_project: start_project, - actions: [{ action: :delete, - file_path: path }]) + def delete_file(user, path, **options) + options[:user] = user + options[:actions] = [{ action: :delete, file_path: path }] + + multi_action(**options) end - # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, start_branch_name: nil, start_project: project) + GitOperationService.new(user, self).with_branch( branch_name, start_branch_name: start_branch_name, start_project: start_project) do |start_commit| - index = rugged.index - parents = if start_commit - index.read_tree(start_commit.raw_commit.tree) - [start_commit.sha] - else - [] - end + index = Gitlab::Git::Index.new(raw_repository) - actions.each do |act| - git_action(index, act) + if start_commit + index.read_tree(start_commit.raw_commit.tree) + parents = [start_commit.sha] + else + parents = [] + end + + actions.each do |options| + index.public_send(options.delete(:action), options) end options = { - tree: index.write_tree(rugged), + tree: index.write_tree, message: message, parents: parents } @@ -1166,30 +1091,6 @@ class Repository blob_data_at(sha, '.gitlab-ci.yml') end - protected - - def tree_entry_at(branch_name, path) - branch_exists?(branch_name) && - # tree_entry is private - raw_repository.send(:tree_entry, commit(branch_name), path) - end - - def check_tree_entry_for_dir(branch_name, path) - return unless branch_exists?(branch_name) - - entry = tree_entry_at(branch_name, path) - - return unless entry - - if entry[:type] == :blob - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists as a file") - else - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists") - end - end - private def blob_data_at(sha, path) @@ -1200,58 +1101,6 @@ class Repository blob.data end - def git_action(index, action) - path = normalize_path(action[:file_path]) - - if action[:action] == :move - previous_path = normalize_path(action[:previous_path]) - end - - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(path)[:mode] - when :move - index.get(previous_path)[:mode] - end - mode ||= 0o100644 - - index.remove(previous_path) if action[:action] == :move - - content = if action[:encoding] == 'base64' - Base64.decode64(action[:content]) - else - action[:content] - end - - detect = CharlockHolmes::EncodingDetector.new.detect(content) if content - - unless detect && detect[:type] == :binary - # When writing to the repo directly as we are doing here, - # the `core.autocrlf` config isn't taken into account. - content.gsub!("\r\n", "\n") if self.autocrlf - end - - oid = rugged.write(content, :blob) - - index.add(path: path, oid: oid, mode: mode) - when :delete - index.remove(path) - end - end - - def normalize_path(path) - pathname = Gitlab::Git::PathHelper.normalize_path(path) - - if pathname.each_filename.include?('..') - raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') - end - - pathname.to_s - end - def refs_directory_exists? return false unless path_with_namespace diff --git a/app/models/service.rb b/app/models/service.rb index facaaf9b331..3ef4cbead10 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -210,7 +210,7 @@ class Service < ActiveRecord::Base end def self.available_services_names - %w[ + service_names = %w[ asana assembla bamboo @@ -238,6 +238,9 @@ class Service < ActiveRecord::Base slack teamcity ] + service_names << 'mock_ci' if Rails.env.development? + + service_names.sort_by(&:downcase) end def self.build_from_template(project_id, template) diff --git a/app/models/user.rb b/app/models/user.rb index 40264401b53..6fb5ac4a4ef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -474,7 +474,7 @@ class User < ActiveRecord::Base Group.member_descendants(id) end - def nested_projects + def nested_groups_projects Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). member_descendants(id) end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 858de5f0538..083ffdc634c 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,7 +1,7 @@ module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir( + repository.create_dir( current_user, @file_path, message: @commit_message, diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 88dd7bbaedb..65b5537fb68 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,13 +1,12 @@ module Files class CreateService < Files::BaseService def commit - repository.commit_file( + repository.create_file( current_user, @file_path, @file_content, message: @commit_message, branch_name: @target_branch, - update: false, author_email: @author_email, author_name: @author_name, start_project: @start_project, @@ -17,6 +16,10 @@ module Files def validate super + if @file_content.nil? + raise_error("You must provide content.") + end + if @file_path =~ Gitlab::Regex.directory_traversal_regex raise_error( 'Your changes could not be committed, because the file name ' + diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb index c3be806a42d..e294659bc98 100644 --- a/app/services/files/destroy_service.rb +++ b/app/services/files/destroy_service.rb @@ -1,7 +1,7 @@ module Files class DestroyService < Files::BaseService def commit - repository.remove_file( + repository.delete_file( current_user, @file_path, message: @commit_message, diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index af6da5b9d56..0609c6219e7 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -2,6 +2,8 @@ module Files class MultiService < Files::BaseService class FileChangedError < StandardError; end + ACTIONS = %w[create update delete move].freeze + def commit repository.multi_action( user: current_user, @@ -21,10 +23,19 @@ module Files super params[:actions].each_with_index do |action, index| + if ACTIONS.include?(action[:action].to_s) + action[:action] = action[:action].to_sym + else + raise_error("Unknown action type `#{action[:action]}`.") + end + unless action[:file_path].present? raise_error("You must specify a file_path.") end + action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') + action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') + regex_check(action[:file_path]) regex_check(action[:previous_path]) if action[:previous_path] @@ -43,8 +54,6 @@ module Files validate_delete(action) when :move validate_move(action, index) - else - raise_error("Unknown action type `#{action[:action]}`.") end end end @@ -92,6 +101,20 @@ module Files if repository.blob_at_branch(params[:branch], action[:file_path]) raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") end + + if action[:content].nil? + raise_error("You must provide content.") + end + end + + def validate_update(action) + if action[:content].nil? + raise_error("You must provide content.") + end + + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") + end end def validate_delete(action) @@ -114,11 +137,5 @@ module Files params[:actions][index][:content] = blob.data end end - - def validate_update(action) - if file_has_changed? - raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.") - end - end end end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index a71fe61a4b6..54e1aaf3f67 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -18,6 +18,10 @@ module Files def validate super + if @file_content.nil? + raise_error("You must provide content.") + end + if file_has_changed? raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 2e2d7f884ac..497fdb09cdc 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -18,7 +18,8 @@ module Groups end group.children.each do |group| - DestroyService.new(group, current_user).async_execute + # This needs to be synchronous since the namespace gets destroyed below + DestroyService.new(group, current_user).execute end group.really_destroy! diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 3da1b657a41..fac3ac7a4c7 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,6 +6,8 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService + MergeError = Class.new(StandardError) + attr_reader :merge_request, :source def execute(merge_request) @@ -27,6 +29,8 @@ module MergeRequests success end end + rescue MergeError => e + log_merge_error(e.message, save_message_on_model: true) end private @@ -42,19 +46,13 @@ module MergeRequests commit_id = repository.merge(current_user, source, merge_request, options) - if commit_id - merge_request.update(merge_commit_sha: commit_id) - else - log_merge_error('Conflicts detected during merge', save_message_on_model: true) - false - end + raise MergeError, 'Conflicts detected during merge' unless commit_id + + merge_request.update(merge_commit_sha: commit_id) rescue GitHooksService::PreReceiveError => e - log_merge_error(e.message, save_message_on_model: true) - false + raise MergeError, e.message rescue StandardError => e - merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") - log_merge_error(e.message) - false + raise MergeError, "Something went wrong during merge: #{e.message}" ensure merge_request.update(in_progress_merge_commit_sha: nil) end diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index 5616edf8b4a..5081dd5a0c4 100644 --- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -24,7 +24,11 @@ module MergeRequests pipeline_merge_requests(pipeline) do |merge_request| next unless merge_request.merge_when_build_succeeds? - next unless merge_request.mergeable? + + unless merge_request.mergeable? + todo_service.merge_request_became_unmergeable(merge_request) + next + end MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index fad741531ea..d9370bbb598 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -115,11 +115,23 @@ module Users # Returns a union query of projects that the user is authorized to access def project_authorizations_union relations = [ + # Personal projects user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - user.groups_projects.select_for_project_authorization, + + # Projects the user is a member of user.projects.select_for_project_authorization, + + # Projects of groups the user is a member of + user.groups_projects.select_for_project_authorization, + + # Projects of subgroups of groups the user is a member of + user.nested_groups_projects.select_for_project_authorization, + + # Projects shared with groups the user is a member of user.groups.joins(:shared_projects).select_for_project_authorization, - user.nested_projects.select_for_project_authorization + + # Projects shared with subgroups of groups the user is a member of + user.nested_groups.joins(:shared_projects).select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 86f317dcd18..e84944ed411 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -27,10 +27,6 @@ class ArtifactUploader < GitlabUploader File.join(self.class.artifacts_cache_path, @build.artifacts_path) end - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - def filename file.try(:filename) end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index cfcb877cc3e..6aa1f5a8c50 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -4,6 +4,6 @@ class AttachmentUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index 265cea2d2c6..b4c393c6f2c 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -4,7 +4,7 @@ class AvatarUploader < GitlabUploader storage :file def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{base_dir}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def exists? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 23b7318827c..0d2edaeff3b 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -4,15 +4,12 @@ class FileUploader < GitlabUploader storage :file - attr_accessor :project, :secret + attr_accessor :project + attr_reader :secret def initialize(project, secret = nil) @project = project - @secret = secret || self.class.generate_secret - end - - def base_dir - "uploads" + @secret = secret || generate_secret end def store_dir @@ -23,10 +20,6 @@ class FileUploader < GitlabUploader File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def secure_url - File.join("/uploads", @secret, file.filename) - end - def to_markdown to_h[:markdown] end @@ -35,17 +28,23 @@ class FileUploader < GitlabUploader filename = image_or_video? ? self.file.basename : self.file.filename escaped_filename = filename.gsub("]", "\\]") - markdown = "[#{escaped_filename}](#{self.secure_url})" + markdown = "[#{escaped_filename}](#{secure_url})" markdown.prepend("!") if image_or_video? || dangerous? { alt: filename, - url: self.secure_url, + url: secure_url, markdown: markdown } end - def self.generate_secret + private + + def generate_secret SecureRandom.hex end + + def secure_url + File.join('/uploads', @secret, file.filename) + end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 02d7c601d6c..bd7de4ed562 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -1,4 +1,14 @@ class GitlabUploader < CarrierWave::Uploader::Base + def self.base_dir + 'uploads' + end + + delegate :base_dir, to: :class + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + # Reduce disk IO def move_to_cache true diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index bee311583ea..7635c20ab3a 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -27,6 +27,8 @@ module UploaderHelper extension_match?(DANGEROUS_EXT) end + private + def extension_match?(extensions) return false unless file @@ -40,8 +42,4 @@ module UploaderHelper extensions.include?(extension.downcase) end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end end diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index deb62845e1c..d4d166ab7b6 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -15,6 +15,8 @@ %td = runner.description %td + = runner.version + %td - if runner.shared? n/a - else diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index d725e477044..7d26864d0f3 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -67,6 +67,7 @@ %th Type %th Runner token %th Description + %th Version %th Projects %th Jobs %th Tags diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index dc76599b776..0dbb0ca6958 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 4f36a4a1c73..b82b933c3ad 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -5,6 +5,8 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } + - if @projects.any? || params[:filter_projects] = render 'dashboard/projects_head' diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index 71cc4d87b1f..c442cf056c3 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do + = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/groups/_head.html.haml b/app/views/groups/_head.html.haml index 6b296ea8dea..873504099d4 100644 --- a/app/views/groups/_head.html.haml +++ b/app/views/groups/_head.html.haml @@ -3,7 +3,7 @@ = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: container_class } - = nav_link(path: 'groups#show', html_options: { class: 'home' }) do + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Group Home' do %span Home @@ -12,8 +12,3 @@ = link_to activity_group_path(@group), title: 'Activity' do %span Activity - - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - %span - Members diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 8cb56443191..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,5 +1,4 @@ - page_title "Members" -= render 'groups/head' .project-members-page.prepend-top-default %h4 diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml index 8610ae7e0ef..be809083139 100644 --- a/app/views/groups/subgroups.html.haml +++ b/app/views/groups/subgroups.html.haml @@ -1,5 +1,6 @@ - @no_container = true += render 'head' = render 'groups/home_panel' .groups-header{ class: container_class } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0b8388cbff3..c28661c2351 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,6 +36,10 @@ = icon('bell fw') %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } = todos_count_format(todos_pending_count) + - if current_user.can_create_project? + %li + = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('plus fw') - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -61,12 +65,12 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - %h1.title= title - .header-logo = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo + %h1.title= title + = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index e0742d70fac..a6e96942021 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -5,7 +5,7 @@ .fade-right = icon('angle-right') %ul.nav-links.scrolling-tabs - = nav_link(path: ['groups#show', 'groups#activity', 'group_members#index'], html_options: { class: 'home' }) do + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do = link_to group_path(@group), title: 'Home' do %span Group @@ -21,3 +21,7 @@ Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + %span + Members diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index b10f5fc08e2..903b957c26b 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -101,5 +101,3 @@ $("#created-personal-access-token").click(function() { this.select(); }); - - $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000); diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 0ea733cb978..4268337fd6d 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -4,7 +4,7 @@ .nav-block.activity-filter-block - if current_user .controls - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do + = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do = icon('rss') = render 'shared/event_filter' diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index e63bdb38bd8..d3c3e40d518 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -12,12 +12,16 @@ .form-group = label_tag :branch_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' + = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' .help-block.text-danger.js-branch-name-error .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10 - = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' + = hidden_field_tag :ref, params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } }) .help-block Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 27e81c2bec3..7eb17e887e7 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,4 +1,4 @@ -.content-block.build-header +.content-block.build-header.top-area .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false Job @@ -16,7 +16,10 @@ - if @build.user = render "user" = time_ago_with_tooltip(@build.created_at) - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + .nav-controls + - if can?(current_user, :create_issue, @project) && @build.failed? + = link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml deleted file mode 100644 index 3475fa5f960..00000000000 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ /dev/null @@ -1,92 +0,0 @@ -- status = pipeline.status -- show_commit = local_assigns.fetch(:show_commit, true) -- show_branch = local_assigns.fetch(:show_branch, true) - -%tr.commit - %td.commit-link - = render 'ci/status/badge', status: pipeline.detailed_status(current_user) - - %td - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do - %span.pipeline-id ##{pipeline.id} - %span by - - if pipeline.user - = user_avatar(user: pipeline.user, size: 20) - - else - %span.api.monospace API - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck - - %td.branch-commit - - if pipeline.ref && show_branch - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name" - - if show_commit - .icon-container.commit-icon - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace" - - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch - - %td - = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph' - - %td - - if pipeline.duration - %p.duration - = custom_icon("icon_timer") - = duration_in_numbers(pipeline.duration) - - if pipeline.finished_at - %p.finished-at - = icon("calendar") - #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)} - - %td.pipeline-actions.hidden-xs - .controls.pull-right - - artifacts = pipeline.builds.latest.with_artifacts_not_expired - - actions = pipeline.manual_actions - - if artifacts.present? || actions.any? - .btn-group.inline - - if actions.any? - .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' } - = custom_icon('icon_play') - = icon('caret-down', 'aria-hidden' => 'true') - %ul.dropdown-menu.dropdown-menu-align-right - - actions.each do |build| - %li - = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do - = custom_icon('icon_play') - %span= build.name - - if artifacts.present? - .btn-group - %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' } - = icon("download") - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - artifacts.each do |build| - %li - = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do - = icon("download") - %span Download '#{build.name}' artifacts - - - if can?(current_user, :update_pipeline, pipeline.project) - .cancel-retry-btns.inline - - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do - = icon("repeat") - - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do - = icon("remove") diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 1dbfe830d52..f809c52c367 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -10,10 +10,10 @@ - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } } = old_path → - %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } + %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } = new_path - else %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 9db46f0b1fc..e442e6e9a09 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -5,4 +5,6 @@ .panel-body %p Learn how to upload your static site and have it served by - GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. + GitLab by following the + = succeed '.' do + = link_to 'documentation on GitLab Pages', help_page_path('user/project/pages/index.md'), target: '_blank' diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 55202725b9e..14a270a3039 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,11 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref + = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch + = dropdown_tag(params[:ref] || @project.default_branch, + options: { toggle_class: 'js-branch-select wide', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches", + data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index e2f132f7742..7f9a44e565f 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" .flex-list{ class: container_class } - .top-area.flex-row + .top-area.adjust .nav-text.row-main-content Tags give the ability to mark specific points in history as being important diff --git a/app/views/shared/icons/_icon_customization.svg b/app/views/shared/icons/_icon_customization.svg new file mode 100644 index 00000000000..eb1f8ba129b --- /dev/null +++ b/app/views/shared/icons/_icon_customization.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg> diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8e04b50bb8a..62f09cc2dc1 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -82,7 +82,7 @@ %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} - .pull-right + .pull-right.filter-dropdown-container = render 'shared/sort_dropdown' - if @bulk_edit diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index dc2fea450bd..c130f3d9e17 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -98,6 +98,7 @@ Snippets %div{ class: container_class } + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/changelogs/unreleased/14748-runner-version-in-admin-views.yml b/changelogs/unreleased/14748-runner-version-in-admin-views.yml new file mode 100644 index 00000000000..2478a81c824 --- /dev/null +++ b/changelogs/unreleased/14748-runner-version-in-admin-views.yml @@ -0,0 +1,4 @@ +--- +title: Add runner version to /admin/runners view +merge_request: 8733 +author: Jonathon Reinhart diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml new file mode 100644 index 00000000000..fa89d94e0f3 --- /dev/null +++ b/changelogs/unreleased/1937-https-clone-url-username.yml @@ -0,0 +1,4 @@ +--- +title: Add the Username to the HTTP(S) clone URL of a Repository +merge_request: 9347 +author: Jan Christophersen diff --git a/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml new file mode 100644 index 00000000000..f7c856040e0 --- /dev/null +++ b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml @@ -0,0 +1,4 @@ +--- +title: Make Git history follow renames again by performing the --skip in Ruby +merge_request: +author: diff --git a/changelogs/unreleased/25920-create-issue-from-failing-build.yml b/changelogs/unreleased/25920-create-issue-from-failing-build.yml new file mode 100644 index 00000000000..580d1074aa7 --- /dev/null +++ b/changelogs/unreleased/25920-create-issue-from-failing-build.yml @@ -0,0 +1,4 @@ +--- +title: Add button to create issue for failing build +merge_request: 9391 +author: Alex Sanford diff --git a/changelogs/unreleased/27354-navigation-new-button.yml b/changelogs/unreleased/27354-navigation-new-button.yml new file mode 100644 index 00000000000..62cac9bbbd3 --- /dev/null +++ b/changelogs/unreleased/27354-navigation-new-button.yml @@ -0,0 +1,4 @@ +--- +title: Re-add the New Project button in nav bar +merge_request: +author: diff --git a/changelogs/unreleased/27840-improve-search-bar-experience.yml b/changelogs/unreleased/27840-improve-search-bar-experience.yml new file mode 100644 index 00000000000..87b1f0c5572 --- /dev/null +++ b/changelogs/unreleased/27840-improve-search-bar-experience.yml @@ -0,0 +1,4 @@ +--- +title: Enhanced filter issues layout for better mobile experiance +merge_request: 9280 +author: Pratik Borsadiya diff --git a/changelogs/unreleased/27934-left-align-logo.yml b/changelogs/unreleased/27934-left-align-logo.yml new file mode 100644 index 00000000000..d4e5e169465 --- /dev/null +++ b/changelogs/unreleased/27934-left-align-logo.yml @@ -0,0 +1,4 @@ +--- +title: Left align logo +merge_request: +author: diff --git a/changelogs/unreleased/27989-disable-counting-tags.yml b/changelogs/unreleased/27989-disable-counting-tags.yml deleted file mode 100644 index 988785ac454..00000000000 --- a/changelogs/unreleased/27989-disable-counting-tags.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable unused tags count cache for Projects, Builds and Runners -merge_request: -author: diff --git a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml b/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml deleted file mode 100644 index d70b5ef8fd5..00000000000 --- a/changelogs/unreleased/28093-snippet-and-issue-spam-check-on-edit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Spam check and reCAPTCHA improvements -merge_request: -author: diff --git a/changelogs/unreleased/28357-colon-search.yml b/changelogs/unreleased/28357-colon-search.yml deleted file mode 100644 index 4bbb0dc12b2..00000000000 --- a/changelogs/unreleased/28357-colon-search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow searching issues for strings containing colons -merge_request: -author: diff --git a/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml new file mode 100644 index 00000000000..faf1e89ed94 --- /dev/null +++ b/changelogs/unreleased/28366-renamed-file-tooltip-contains-html.yml @@ -0,0 +1,4 @@ +--- +title: Remove markup that was showing in tooltip for renamed files +merge_request: 9374 +author: diff --git a/changelogs/unreleased/28723-consistent-handling-indexof.yml b/changelogs/unreleased/28723-consistent-handling-indexof.yml new file mode 100644 index 00000000000..95d6181d5fa --- /dev/null +++ b/changelogs/unreleased/28723-consistent-handling-indexof.yml @@ -0,0 +1,4 @@ +--- +title: Keep consistent in handling indexOf results +merge_request: 9531 +author: Takuya Noguchi diff --git a/changelogs/unreleased/6073_project_api.yml b/changelogs/unreleased/6073_project_api.yml new file mode 100644 index 00000000000..fd6792a406e --- /dev/null +++ b/changelogs/unreleased/6073_project_api.yml @@ -0,0 +1,4 @@ +--- +title: 'API project create: Make name or path required' +merge_request: 9416 +author: diff --git a/changelogs/unreleased/add-issues-tooltip.yml b/changelogs/unreleased/add-issues-tooltip.yml deleted file mode 100644 index 58adb6c6b5a..00000000000 --- a/changelogs/unreleased/add-issues-tooltip.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disabled tooltip on add issues button in usse boards -merge_request: -author: diff --git a/changelogs/unreleased/api-empty-return.yml b/changelogs/unreleased/api-empty-return.yml new file mode 100644 index 00000000000..7810e83eb0e --- /dev/null +++ b/changelogs/unreleased/api-empty-return.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Return 204 for all delete endpoints' +merge_request: 9397 +author: Robert Schilling diff --git a/changelogs/unreleased/commit-search-ui-fix.yml b/changelogs/unreleased/commit-search-ui-fix.yml deleted file mode 100644 index 4a5c2cf6090..00000000000 --- a/changelogs/unreleased/commit-search-ui-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed commit search UI -merge_request: -author: diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml new file mode 100644 index 00000000000..e646a6a17b7 --- /dev/null +++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml @@ -0,0 +1,4 @@ +--- +title: Add Runner's registration/deletion v4 API +merge_request: 9246 +author: diff --git a/changelogs/unreleased/fix-mr-size-with-over-100-files.yml b/changelogs/unreleased/fix-mr-size-with-over-100-files.yml deleted file mode 100644 index eecf3c99a75..00000000000 --- a/changelogs/unreleased/fix-mr-size-with-over-100-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix MR changes tab size count when there are over 100 files in the diff -merge_request: -author: diff --git a/changelogs/unreleased/issue-tags-layout.yml b/changelogs/unreleased/issue-tags-layout.yml new file mode 100644 index 00000000000..abf4a609932 --- /dev/null +++ b/changelogs/unreleased/issue-tags-layout.yml @@ -0,0 +1,4 @@ +--- +title: Fix 'New Tag' layout on Tags page +merge_request: +author: Robert Marcano diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml new file mode 100644 index 00000000000..916e47d36a9 --- /dev/null +++ b/changelogs/unreleased/issue_24815.yml @@ -0,0 +1,4 @@ +--- +title: Fix issuable stale object error handler for js when updating tasklists +merge_request: +author: diff --git a/changelogs/unreleased/issue_25112.yml b/changelogs/unreleased/issue_25112.yml deleted file mode 100644 index c43d2732b9a..00000000000 --- a/changelogs/unreleased/issue_25112.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable invalid service templates -merge_request: -author: diff --git a/changelogs/unreleased/issue_28051_2.yml b/changelogs/unreleased/issue_28051_2.yml deleted file mode 100644 index 8cc32ad8493..00000000000 --- a/changelogs/unreleased/issue_28051_2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use default branch as target_branch when parameter is missing -merge_request: -author: diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml new file mode 100644 index 00000000000..24c6366177f --- /dev/null +++ b/changelogs/unreleased/mock-ci-service.yml @@ -0,0 +1,4 @@ +--- +title: Add Mock CI service/integration for development +merge_request: +author: diff --git a/changelogs/unreleased/moving-issue-with-two-list-labels.yml b/changelogs/unreleased/moving-issue-with-two-list-labels.yml new file mode 100644 index 00000000000..d5ea81e3810 --- /dev/null +++ b/changelogs/unreleased/moving-issue-with-two-list-labels.yml @@ -0,0 +1,4 @@ +--- +title: Removes label when moving issue to another list that it is currently in +merge_request: +author: diff --git a/changelogs/unreleased/mr-diff-comment-button.yml b/changelogs/unreleased/mr-diff-comment-button.yml new file mode 100644 index 00000000000..1dc6ed1c495 --- /dev/null +++ b/changelogs/unreleased/mr-diff-comment-button.yml @@ -0,0 +1,4 @@ +--- +title: Improved diff comment button UX +merge_request: +author: diff --git a/changelogs/unreleased/only-create-unmergeable-todo-once.yml b/changelogs/unreleased/only-create-unmergeable-todo-once.yml new file mode 100644 index 00000000000..e675ed945ad --- /dev/null +++ b/changelogs/unreleased/only-create-unmergeable-todo-once.yml @@ -0,0 +1,4 @@ +--- +title: Only create unmergeable todos once when MR fails to merge +merge_request: +author: diff --git a/changelogs/unreleased/pages-0-3-2.yml b/changelogs/unreleased/pages-0-3-2.yml deleted file mode 100644 index f660379f2e6..00000000000 --- a/changelogs/unreleased/pages-0-3-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Upgrade GitLab Pages to v0.3.2 -merge_request: -author: diff --git a/changelogs/unreleased/remove-jquery-ui-plugins.yml b/changelogs/unreleased/remove-jquery-ui-plugins.yml new file mode 100644 index 00000000000..c768f702ba2 --- /dev/null +++ b/changelogs/unreleased/remove-jquery-ui-plugins.yml @@ -0,0 +1,4 @@ +--- +title: Removed jQuery UI highlight & autocomplete +merge_request: +author: diff --git a/changelogs/unreleased/rss-btn-alignment-fix.yml b/changelogs/unreleased/rss-btn-alignment-fix.yml new file mode 100644 index 00000000000..c8f57ec0b7c --- /dev/null +++ b/changelogs/unreleased/rss-btn-alignment-fix.yml @@ -0,0 +1,4 @@ +--- +title: Fixed RSS button alignment on activity pages +merge_request: +author: diff --git a/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml new file mode 100644 index 00000000000..57f1474093a --- /dev/null +++ b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml @@ -0,0 +1,4 @@ +--- +title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise +merge_request: +author: diff --git a/changelogs/unreleased/ssh-key-paste.yml b/changelogs/unreleased/ssh-key-paste.yml new file mode 100644 index 00000000000..1e34ef60f6e --- /dev/null +++ b/changelogs/unreleased/ssh-key-paste.yml @@ -0,0 +1,4 @@ +--- +title: SSH key field updates title after pasting key +merge_request: +author: diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml new file mode 100644 index 00000000000..0a60b4d46a3 --- /dev/null +++ b/changelogs/unreleased/unified-member-api-response.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Return 400 for all validation erros in the mebers API' +merge_request: 9523 +author: Robert Schilling diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml new file mode 100644 index 00000000000..acc42bf00b1 --- /dev/null +++ b/changelogs/unreleased/update-vue-2-1.yml @@ -0,0 +1,4 @@ +--- +title: update Vue to v2.1.10 +merge_request: 9386 +author: diff --git a/changelogs/unreleased/user-callouts.yml b/changelogs/unreleased/user-callouts.yml new file mode 100644 index 00000000000..f6ce06a3d8f --- /dev/null +++ b/changelogs/unreleased/user-callouts.yml @@ -0,0 +1,4 @@ +--- +title: Added user callouts to the projects dashboard and user profile +merge_request: +author: diff --git a/changelogs/unreleased/zj-fix-slash-command-labels.yml b/changelogs/unreleased/zj-fix-slash-command-labels.yml deleted file mode 100644 index 93b7194dd4e..00000000000 --- a/changelogs/unreleased/zj-fix-slash-command-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Chat slash commands show labels correctly -merge_request: -author: diff --git a/config/webpack.config.js b/config/webpack.config.js index e754f68553a..a71ec0c5f52 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -87,7 +87,7 @@ var config = { 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', + 'vue$': 'vue/dist/vue.common.js', } } } diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 747901dd634..aea0a72b633 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics issue.project.repository.add_branch(@user, branch_name, 'master') - options = { - committer: issue.project.repository.user_to_committer(@user), - author: issue.project.repository.user_to_committer(@user), - commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } - - commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options) + commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name) issue.project.repository.commit(commit_sha) - GitPushService.new(issue.project, @user, oldrev: issue.project.repository.commit("master").sha, diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/19_nested_groups.rb new file mode 100644 index 00000000000..d8dddc3fee9 --- /dev/null +++ b/db/fixtures/development/19_nested_groups.rb @@ -0,0 +1,69 @@ +require './spec/support/sidekiq' + +def create_group_with_parents(user, full_path) + parent_path = nil + group = nil + + until full_path.blank? + path, _, full_path = full_path.partition('/') + + if parent_path + parent = Group.find_by_full_path(parent_path) + + parent_path += '/' + parent_path += path + + group = Groups::CreateService.new(user, path: path, parent_id: parent.id).execute + else + parent_path = path + + group = Group.find_by_full_path(parent_path) || + Groups::CreateService.new(user, path: path).execute + end + end + + group +end + +Sidekiq::Testing.inline! do + Gitlab::Seeder.quiet do + project_urls = [ + 'https://android.googlesource.com/platform/hardware/broadcom/libbt.git', + 'https://android.googlesource.com/platform/hardware/broadcom/wlan.git', + 'https://android.googlesource.com/platform/hardware/bsp/bootloader/intel/edison-u-boot.git', + 'https://android.googlesource.com/platform/hardware/bsp/broadcom.git', + 'https://android.googlesource.com/platform/hardware/bsp/freescale.git', + 'https://android.googlesource.com/platform/hardware/bsp/imagination.git', + 'https://android.googlesource.com/platform/hardware/bsp/intel.git', + 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.1.git', + 'https://android.googlesource.com/platform/hardware/bsp/kernel/common/v4.4.git' + ] + + user = User.admins.first + + project_urls.each_with_index do |url, i| + full_path = url.sub('https://android.googlesource.com/', '') + full_path = full_path.sub(/\.git\z/, '') + full_path, _, project_path = full_path.rpartition('/') + group = Group.find_by_full_path(full_path) || create_group_with_parents(user, full_path) + + params = { + import_url: url, + namespace_id: group.id, + path: project_path, + name: project_path, + description: FFaker::Lorem.sentence, + visibility_level: Gitlab::VisibilityLevel.values.sample + } + + project = Projects::CreateService.new(user, params).execute + project.send(:_run_after_commit_queue) + + if project.valid? + print '.' + else + print 'F' + end + end + end +end diff --git a/db/migrate/20160610201627_migrate_users_notification_level.rb b/db/migrate/20160610201627_migrate_users_notification_level.rb index ce4f00e25fa..cd8b505de9f 100644 --- a/db/migrate/20160610201627_migrate_users_notification_level.rb +++ b/db/migrate/20160610201627_migrate_users_notification_level.rb @@ -1,4 +1,6 @@ class MigrateUsersNotificationLevel < ActiveRecord::Migration + DOWNTIME = false + # Migrates only users who changed their default notification level :participating # creating a new record on notification settings table diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a6300e18dc0..28e413ef447 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your project, you can disable it from your project's settings. Read the user guide on how to achieve that. +## Disable Container Registry but use GitLab as an auth endpoint + +You can disable the embedded Container Registry to use an external one, but +still use GitLab as an auth endpoint. + +**Omnibus GitLab** +1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations: + + ```ruby + registry['enable'] = false + gitlab_rails['registry_enabled'] = true + gitlab_rails['registry_host'] = "registry.gitlab.example.com" + gitlab_rails['registry_port'] = "5005" + gitlab_rails['registry_api_url'] = "http://localhost:5000" + gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key" + gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry" + gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`: + + ``` + ## Container Registry + + registry: + enabled: true + host: "registry.gitlab.example.com" + port: "5005" + api_url: "http://localhost:5000" + path: /var/opt/gitlab/gitlab-rails/shared/registry + key: /var/opt/gitlab/gitlab-rails/certificate.key + issuer: omnibus-gitlab-issuer + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + ## Storage limitations Currently, there is no storage limitation, which means a user can upload an diff --git a/doc/api/README.md b/doc/api/README.md index b334ca46caf..1c3b2ad0fbc 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -159,6 +159,7 @@ The following table shows the possible return codes for API requests. | Return values | Description | | ------------- | ----------- | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | +| `204 OK` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. | | `304 Not Modified` | Indicates that the resource has not been modified since the last request. | | `400 Bad Request` | A required attribute of the API request is missing, e.g., the title of an issue is not given. | diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 58092bdd400..c6fd8c5fa53 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -178,27 +178,6 @@ Parameters: curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344 ``` -Example Response: - -```json -{ - "id": 344, - "name": "blowfish", - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/root" - }, - "created_at": "2016-06-17T17:47:29.266Z", - "updated_at": "2016-06-17T17:47:29.266Z", - "awardable_id": 80, - "awardable_type": "Issue" -} -``` - ## Award Emoji on Notes The endpoints documented above are available for Notes as well. Notes @@ -350,25 +329,4 @@ Parameters: curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345 ``` -Example Response: - -```json -{ - "id": 345, - "name": "rocket", - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/root" - }, - "created_at": "2016-06-17T19:59:55.888Z", - "updated_at": "2016-06-17T19:59:55.888Z", - "awardable_id": 1, - "awardable_type": "Note" -} -``` - [ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575 diff --git a/doc/api/boards.md b/doc/api/boards.md index c83db6df80c..f80b98f960b 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -226,16 +226,3 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1 ``` -Example response: - -```json -{ - "id" : 1, - "label" : { - "name" : "Testing", - "color" : "#F0AD4E", - "description" : null - }, - "position" : 1 -} -``` diff --git a/doc/api/branches.md b/doc/api/branches.md index 765ca439720..f29a8518945 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -244,14 +244,6 @@ In case of an error, an explaining message is provided. curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch" ``` -Example response: - -```json -{ - "branch_name": "newbranch" -} -``` - ## Delete merged branches Will delete all branches that are merged into the project's default branch. diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index a3e9c01f335..fecfb142ab1 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -138,17 +138,3 @@ DELETE /broadcast_messages/:id ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1 ``` - -Example response: - -```json -{ - "message":"Update message", - "starts_at":"2016-08-26T00:41:35.060Z", - "ends_at":"2016-08-26T01:41:35.060Z", - "color":"#000", - "font":"#FFFFFF", - "id":1, - "active": true -} -``` diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index b6459971420..6adefe8c58c 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -106,13 +106,3 @@ DELETE /projects/:id/triggers/:token ``` curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` - -```json -{ - "created_at": "2015-12-23T16:25:56.760Z", - "deleted_at": "2015-12-24T12:32:20.100Z", - "last_used": null, - "token": "7b9148c158980bbd9bcea92c17522d", - "updated_at": "2015-12-24T12:32:20.100Z" -} -``` diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index 917e9773913..c21d5ab2787 100644 --- a/doc/api/build_variables.md +++ b/doc/api/build_variables.md @@ -119,10 +119,3 @@ DELETE /projects/:id/variables/:key ``` curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1" ``` - -```json -{ - "key": "VARIABLE_1", - "value": "VALUE_1" -} -``` diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 39afc4b2df5..d03d94cb867 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -152,18 +152,6 @@ DELETE /projects/:id/deploy_keys/:key_id curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13" ``` -Example response: - -```json -{ - "id": 6, - "deploy_key_id": 14, - "project_id": 1, - "created_at" : "2015-08-29T12:50:57.259Z", - "updated_at" : "2015-08-29T12:50:57.259Z" -} -``` - ## Enable a deploy key Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful. diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md index e0ee20d9610..e510f723e26 100644 --- a/doc/api/enviroments.md +++ b/doc/api/enviroments.md @@ -108,14 +108,3 @@ DELETE /projects/:id/environments/:environment_id ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1" ``` - -Example response: - -```json -{ - "id": 1, - "name": "deploy", - "slug": "deploy", - "external_url": "https://deploy.example.gitlab.com" -} -``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 5266077e098..b6798fba0ae 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -581,43 +581,6 @@ POST /projects/:id/issues/:issue_id/unsubscribe curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/unsubscribe ``` -Example response: - -```json -{ - "id": 93, - "iid": 12, - "project_id": 5, - "title": "Incidunt et rerum ea expedita iure quibusdam.", - "description": "Et cumque architecto sed aut ipsam.", - "state": "opened", - "created_at": "2016-04-05T21:41:45.217Z", - "updated_at": "2016-04-07T13:02:37.905Z", - "labels": [], - "milestone": null, - "assignee": { - "name": "Edwardo Grady", - "username": "keyon", - "id": 21, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", - "web_url": "https://gitlab.example.com/keyon" - }, - "author": { - "name": "Vivian Hermann", - "username": "orville", - "id": 11, - "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/orville" - }, - "subscribed": false, - "due_date": null, - "web_url": "http://example.com/example/example/issues/12", - "confidential": false -} -``` - ## Create a todo Manually creates a todo for the current user on an issue. If diff --git a/doc/api/labels.md b/doc/api/labels.md index 8e0855fe9e2..85bd9647a7b 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -131,22 +131,6 @@ DELETE /projects/:id/labels curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug" ``` -Example response: - -```json -{ - "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 -} -``` - ## Edit an existing label Updates an existing label with new name or new color. At least one parameter @@ -239,19 +223,3 @@ POST /projects/:id/labels/:label_id/unsubscribe ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/unsubscribe ``` - -Example response: - -```json -{ - "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/notes.md b/doc/api/notes.md index dced821cc6d..7dc1fd930de 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -123,30 +123,6 @@ Parameters: curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636 ``` -Example Response: - -```json -{ - "id": 636, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-05T22:10:44.164Z", - "system": false, - "noteable_id": 11, - "noteable_type": "Issue" -} -``` - ## Snippets ### List all snippet notes @@ -245,30 +221,6 @@ Parameters: curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659 ``` -Example Response: - -```json -{ - "id": 1659, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-06T16:51:53.239Z", - "system": false, - "noteable_id": 52, - "noteable_type": "Snippet" -} -``` - ## Merge Requests ### List all merge request notes @@ -369,27 +321,3 @@ Parameters: ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602 ``` - -Example Response: - -```json -{ - "id": 1602, - "body": "This is a good idea.", - "attachment": null, - "author": { - "id": 1, - "username": "pipin", - "email": "admin@example.com", - "name": "Pip", - "state": "active", - "created_at": "2013-09-30T13:46:01Z", - "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/pipin" - }, - "created_at": "2016-04-05T22:11:59.923Z", - "system": false, - "noteable_id": 7, - "noteable_type": "MergeRequest" -} -``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 1a8c0ae758f..5de23908b42 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -435,8 +435,8 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `name` | string | yes | The name of the new project | -| `path` | string | no | Custom repository name for new project. By default generated based on name | +| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. | +| `path` | string | yes if name is not provided | Repository name for new project. Generated based on name if not provided (generated lowercased with dashes). | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `description` | string | no | Short project description | | `issues_enabled` | boolean | no | Enable issues for this project | diff --git a/doc/api/runners.md b/doc/api/runners.md index 28610762dca..27d8e7640b2 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -210,18 +210,6 @@ DELETE /runners/:id curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" ``` -Example response: - -```json -{ - "active": true, - "description": "test-1-20150125-test", - "id": 6, - "is_shared": false, - "name": null, -} -``` - ## List project's runners List all runners (specific and shared) available in the project. Shared runners @@ -308,15 +296,3 @@ DELETE /projects/:id/runners/:runner_id ``` curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` - -Example response: - -```json -{ - "active": true, - "description": "test-2016-02-01", - "id": 9, - "is_shared": false, - "name": null -} -``` diff --git a/doc/api/services.md b/doc/api/services.md index fba5da6587d..b030a425a7a 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -810,3 +810,38 @@ GET /projects/:id/services/teamcity [jira-doc]: ../user/project/integrations/jira.md [old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira + + +## MockCI + +Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service. + +This service is only available when your environment is set to development. + +### Create/Edit MockCI service + +Set MockCI service for a project. + +``` +PUT /projects/:id/services/mock-ci +``` + +Parameters: + +- `mock_service_url` (**required**) - http://localhost:4004 + +### Delete MockCI service + +Delete MockCI service for a project. + +``` +DELETE /projects/:id/services/mock-ci +``` + +### Get MockCI service settings + +Get MockCI service settings for a project. + +``` +GET /projects/:id/services/mock-ci +``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 3fb8b73be6d..a9edff799ac 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -125,22 +125,3 @@ Example request: ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2 ``` - -Example response: - -```json -{ - "note_events" : false, - "project_id" : null, - "enable_ssl_verification" : true, - "url" : "https://gitlab.example.com/hook", - "updated_at" : "2015-11-04T20:12:15.931Z", - "issues_events" : false, - "merge_requests_events" : false, - "created_at" : "2015-11-04T20:12:15.931Z", - "service_id" : null, - "id" : 2, - "push_events" : true, - "tag_push_events" : false -} -``` diff --git a/doc/api/tags.md b/doc/api/tags.md index 7f78ffc2390..abeb4bfb40e 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -141,11 +141,6 @@ Parameters: - `id` (required) - The ID of a project - `tag_name` (required) - The name of a tag -```json -{ - "tag_name": "v4.3.0" -} -``` ## Create a new release diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 8af041be234..c178e224cc5 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -41,5 +41,6 @@ changes are in V4: - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) +- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523) - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) -- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
\ No newline at end of file +- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index dd3ba1283f8..a586b095ef5 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1018,7 +1018,7 @@ A simple example: ```yaml job1: - coverage: /Code coverage: \d+\.\d+/ + coverage: '/Code coverage: \d+\.\d+/' ``` ## Git Strategy diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index 2f49b3564ab..b03216fec95 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -2,11 +2,12 @@ This document describes what services we use for testing GitLab and GitLab CI. -We currently use three CI services to test GitLab: +We currently use four CI services to test GitLab: 1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) +4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | |---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| diff --git a/doc/development/frontend.md b/doc/development/frontend.md index ba47998de49..9ba820eaee5 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -238,6 +238,9 @@ readability. See the relevant style guides for our guidelines and for information on linting: - [SCSS][scss-style-guide] +- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related +conventions and enforce them with eslint. See [our current .eslintrc][eslistrc] +for specific rules and patterns. ## Testing @@ -434,3 +437,5 @@ Scenario: Developer can approve merge request [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [vue-resource-repo]: https://github.com/pagekit/vue-resource [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 +[airbnb-js-style-guide]: https://github.com/airbnb/javascript +[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 2d82b09f301..e3568b65b18 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -50,6 +50,12 @@ Notes: asking a GitLab developer to do it once the merge request is merged. - If you branch is more than 500 commits behind `master`, the job will fail and you should rebase your branch upon latest `master`. +- Code reviews for merge requests often consist of multiple iterations of + feedback and fixes. There is no need to update your EE MR after each + iteration. Instead, create an EE MR as soon as you see the + `rake ee_compat_check` job failing and update it after the CE MR is merged. + This helps to identify significant conflicts sooner, but also reduces the + number of times you have to resolve conflicts. ## Possible type of conflicts diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md index da410a8de7a..137154e24f3 100644 --- a/doc/development/ux_guide/users.md +++ b/doc/development/ux_guide/users.md @@ -27,19 +27,19 @@ - **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security. #### Motivations -Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab: +Nazim works for a software development company which currently hires around 80 people. When Nazim first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Nazim felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Nazim began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Nazim explains why he wanted the engineering team to start using GitLab: > “I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.” > -In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab. +In his role as a full-stack web developer, Nazim could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Nazim recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab. > “The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.” > -Undeterred, Steven decided to migrate a couple of projects across to GitLab. +Undeterred, Nazim decided to migrate a couple of projects across to GitLab. > “Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.” @@ -47,17 +47,17 @@ Undeterred, Steven decided to migrate a couple of projects across to GitLab. Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab. -The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab. +The engineering team have been using GitLab CE for around 2 years now. Nazim credits himself as being entirely responsible for his company’s decision to move to GitLab. #### Frustrations ##### Adoption to GitLab has been slow -Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits. +Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Nazim sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Nazim hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits. ##### Missing Features -Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do. +Nazim’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Nazim’s company wants to know if GitLab has a specific feature or does a particular thing, Nazim is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Nazim gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do. ##### Regressions and bugs -Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month. +Nazim often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month. ##### Uses too much RAM and CPU > @@ -65,7 +65,7 @@ Steven often has to calm down his colleagues, when a release contains regression > ##### UI/UX -GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.” +GitLab’s interface initially attracted Nazim when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.” #### Goals * To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration. @@ -121,8 +121,8 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w #### Goals * To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed. -* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily. -* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven. +* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. James and his team want to be able to understand and use these particular features easily. +* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to James. <hr> @@ -144,21 +144,21 @@ James and his team use CI quite heavily for several projects. Whilst they’ve w - **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel. #### Motivations -Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” He’s also an avid reader of GitLab’s blog. +Karolina has been using GitLab.com for around a year. She roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Karolina contributes to open source projects to gain programming experience and to give back to the community. She likes GitLab.com for its free private repositories and range of features which provide her with everything she needs for her personal projects. Karolina is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. She explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” She’s also an avid reader of GitLab’s blog. -Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms. +Karolina works for a software development company which currently hires around 500 people. Karolina would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. She describes management at her company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Karolina is also relatively new to the company so she’s apprehensive about pushing too hard to change version control platforms. #### Frustrations ##### Unable to use GitLab at work -Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab. +Karolina wants to use GitLab at work but isn’t sure how to approach the subject with management. In her current role, she doesn’t feel that she has the authority to request GitLab. ##### Performance -GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog” which has deterred him from running GitLab on his own machine for just hobby / personal projects. +GitLab.com is frequently slow and unavailable. Karolina has also heard that GitLab is a “memory hog” which has deterred her from running GitLab on her own machine for just hobby / personal projects. ##### UX/UI -Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this. +Karolina has an interest in UX and therefore has strong opinions about how GitLab should look and feel. She feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Karolina also enjoys contributing to open-source projects, it’s important to her that GitLab is well designed for public repositories, she doesn’t feel that GitLab currently achieves this. #### Goals -* To develop his programming experience and to learn from other developers. -* To contribute to both his own and other open source projects. +* To develop her programming experience and to learn from other developers. +* To contribute to both her own and other open source projects. * To use a fast and intuitive version control platform.
\ No newline at end of file diff --git a/doc/pages/README.md b/doc/pages/README.md new file mode 100644 index 00000000000..7878bce3f10 --- /dev/null +++ b/doc/pages/README.md @@ -0,0 +1 @@ +This document was moved to [pages/index.md](../user/project/pages/index.md). diff --git a/doc/pages/getting_started_part_one.md b/doc/pages/getting_started_part_one.md index c5b1aa4b654..1d63ccb4d2f 100644 --- a/doc/pages/getting_started_part_one.md +++ b/doc/pages/getting_started_part_one.md @@ -1,266 +1 @@ -# GitLab Pages from A to Z: Part 1 - -- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ - ----- - -This is a comprehensive guide, made for those who want to -publish a website with GitLab Pages but aren't familiar with -the entire process involved. - -To **enable** GitLab Pages for GitLab CE (Community Edition) -and GitLab EE (Enterprise Edition), please read the -[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), -and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). - ->**Note:** -For this guide, we assume you already have GitLab Pages -server up and running for your GitLab instance. - -## What you need to know before getting started - -Before we begin, let's understand a few concepts first. - -### Static sites - -GitLab Pages only supports static websites, meaning, -your output files must be HTML, CSS, and JavaScript only. - -To create your static site, you can either hardcode in HTML, -CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) -to simplify your code and build the static site for you, -which is highly recommendable and much faster than hardcoding. - ---- - -- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) -- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site -- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- Fork an [example project](https://gitlab.com/pages) to build your website based upon - -### GitLab Pages domain - -If you set up a GitLab Pages project on GitLab.com, -it will automatically be accessible under a -[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). -The `namespace` is defined by your username on GitLab.com, -or the group name you created this project under. - ->**Note:** -If you use your own GitLab instance to deploy your -site with GitLab Pages, check with your sysadmin what's your -Pages wildcard domain. This guide is valid for any GitLab instance, -you just need to replace Pages wildcard domain on GitLab.com -(`*.gitlab.io`) with your own. - -#### Practical examples - -**Project Websites:** - -- You created a project called `blog` under your username `john`, -therefore your project URL is `https://gitlab.com/john/blog/`. -Once you enable GitLab Pages for this project, and build your site, -it will be available under `https://john.gitlab.io/blog/`. -- You created a group for all your websites called `websites`, -and a project within this group is called `blog`. Your project -URL is `https://gitlab.com/websites/blog/`. Once you enable -GitLab Pages for this project, the site will live under -`https://websites.gitlab.io/blog/`. - -**User and Group Websites:** - -- Under your username, `john`, you created a project called -`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. -Once you enable GitLab Pages for your project, your website -will be published under `https://john.gitlab.io`. -- Under your group `websites`, you created a project called -`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, -your website will be published under `https://websites.gitlab.io`. - -**General example:** - -- On GitLab.com, a project site will always be available under -`https://namespace.gitlab.io/project-name` -- On GitLab.com, a user or group website will be available under -`https://namespace.gitlab.io/` -- On your GitLab instance, replace `gitlab.io` above with your -Pages server domain. Ask your sysadmin for this information. - -### DNS Records - -A Domain Name System (DNS) web service routes visitors to websites -by translating domain names (such as `www.example.com`) into the -numeric IP addresses (such as `192.0.2.1`) that computers use to -connect to each other. - -A DNS record is created to point a (sub)domain to a certain location, -which can be an IP address or another domain. In case you want to use -GitLab Pages with your own (sub)domain, you need to access your domain's -registrar control panel to add a DNS record pointing it back to your -GitLab Pages site. - -Note that **how to** add DNS records depends on which server your domain -is hosted on. Every control panel has its own place to do it. If you are -not an admin of your domain, and don't have access to your registrar, -you'll need to ask for the technical support of your hosting service -to do it for you. - -To help you out, we've gathered some instructions on how to do that -for the most popular hosting services: - -- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) -- [Bluehost](https://my.bluehost.com/cgi/help/559) -- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) -- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) -- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) -- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) -- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) -- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) -- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) -- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) - -If your hosting service is not listed above, you can just try to -search the web for "how to add dns record on <my hosting service>". - -#### DNS A record - -In case you want to point a root domain (`example.com`) to your -GitLab Pages site, deployed to `namespace.gitlab.io`, you need to -log into your domain's admin control panel and add a DNS `A` record -pointing your domain to Pages' server IP address. For projects on -GitLab.com, this IP is `104.208.235.32`. For projects leaving in -other GitLab instances (CE or EE), please contact your sysadmin -asking for this information (which IP address is Pages server -running on your instance). - -**Practical Example:** - -![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) - -#### DNS CNAME record - -In case you want to point a subdomain (`hello-world.example.com`) -to your GitLab Pages site initially deployed to `namespace.gitlab.io`, -you need to log into your domain's admin control panel and add a DNS -`CNAME` record pointing your subdomain to your website URL -(`namespace.gitlab.io`) address. - -Notice that, despite it's a user or project website, the `CNAME` -should point to your Pages domain (`namespace.gitlab.io`), -without any `/project-name`. - -**Practical Example:** - -![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) - -#### TL;DR - -| From | DNS Record | To | -| ---- | ---------- | -- | -| domain.com | A | 104.208.235.32 | -| subdomain.domain.com | CNAME | namespace.gitlab.io | - -> **Notes**: -> -> - **Do not** use a CNAME record if you want to point your -`domain.com` to your GitLab Pages site. Use an `A` record instead. -> - **Do not** add any special chars after the default Pages -domain. E.g., **do not** point your `subdomain.domain.com` to -`namespace.gitlab.io.` or `namespace.gitlab.io/`. - -### SSL/TLS Certificates - -Every GitLab Pages project on GitLab.com will be available under -HTTPS for the default Pages domain (`*.gitlab.io`). Once you set -up your Pages project with your custom (sub)domain, if you want -it secured by HTTPS, you will have to issue a certificate for that -(sub)domain and install it on your project. - ->**Note:** -Certificates are NOT required to add to your custom -(sub)domain on your GitLab Pages project, though they are -highly recommendable. - -The importance of having any website securely served under HTTPS -is explained on the introductory section of the blog post -[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). - -The reason why certificates are so important is that they encrypt -the connection between the **client** (you, me, your visitors) -and the **server** (where you site lives), through a keychain of -authentications and validations. - -### Issuing Certificates - -GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by -[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) -and self-signed certificates. Of course, -[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), -for security reasons and for having browsers trusting your -site's certificate. - -There are several different kinds of certificates, each one -with certain security level. A static personal website will -not require the same security level as an online banking web app, -for instance. There are a couple Certificate Authorities that -offer free certificates, aiming to make the internet more secure -to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), -which issues certificates trusted by most of browsers, it's open -source, and free to use. Please read through this tutorial to -understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). - -With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), -which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). -Their certs are valid up to 15 years. Read through the tutorial on -[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). - -### Adding certificates to your project - -Regardless the CA you choose, the steps to add your certificate to -your Pages project are the same. - -#### What do you need - -1. A PEM certificate -1. An intermediate certificate -1. A public key - -![Pages project - adding certificates](img/add_certificate_to_pages.png) - -These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. - -#### What's what? - -- A PEM certificate is the certificate generated by the CA, -which needs to be added to the field **Certificate (PEM)**. -- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is -the part of the encryption keychain that identifies the CA. -Usually it's combined with the PEM certificate, but there are -some cases in which you need to add them manually. -[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) -are one of these cases. -- A public key is an encrypted key which validates -your PEM against your domain. - -#### Now what? - -Now that you hopefully understand why you need all -of this, it's simple: - -- Your PEM certificate needs to be added to the first field -- If your certificate is missing its intermediate, copy -and paste the root certificate (usually available from your CA website) -and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), -just jumping a line between them. -- Copy your public key and paste it in the last field - ->**Note:** -**Do not** open certificates or encryption keys in -regular text editors. Always use code editors (such as -Sublime Text, Atom, Dreamweaver, Brackets, etc). - -||| -|:--|--:| -||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| +This document was moved to [another location](../user/project/pages/getting_started_part_one.md). diff --git a/doc/pages/getting_started_part_three.md b/doc/pages/getting_started_part_three.md index ef47abef3a0..1697b5cd6b4 100644 --- a/doc/pages/getting_started_part_three.md +++ b/doc/pages/getting_started_part_three.md @@ -1,383 +1 @@ -# GitLab Pages from A to Z: Part 3 - -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ -- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** - ---- - -## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages - -[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves -numerous purposes, to build, test, and deploy your app -from GitLab through -[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) -methods. You will need it to build your website with GitLab Pages, -and deploy it to the Pages server. - -What this file actually does is telling the -[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts -as you would do from the command line. The Runner acts as your -terminal. GitLab CI tells the Runner which commands to run. -Both are built-in in GitLab, and you don't need to set up -anything for them to work. - -Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) -and GitLab Runner is out of the scope of this guide, but we'll -need to understand just a few things to be able to write our own -`.gitlab-ci.yml` or tweak an existing one. It's an -[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, -with its own syntax. You can always check your CI syntax with -the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). - -**Practical Example:** - -Let's consider you have a [Jekyll](https://jekyllrb.com/) site. -To build it locally, you would open your terminal, and run `jekyll build`. -Of course, before building it, you had to install Jekyll in your computer. -For that, you had to open your terminal and run `gem install jekyll`. -Right? GitLab CI + GitLab Runner do the same thing. But you need to -write in the `.gitlab-ci.yml` the script you want to run so -GitLab Runner will do it for you. It looks more complicated then it -is. What you need to tell the Runner: - -``` -$ gem install jekyll -$ jekyll build -``` - -### Script - -To transpose this script to Yaml, it would be like this: - -```yaml -script: - - gem install jekyll - - jekyll build -``` - -### Job - -So far so good. Now, each `script`, in GitLab is organized by -a `job`, which is a bunch of scripts and settings you want to -apply to that specific task. - -```yaml -job: - script: - - gem install jekyll - - jekyll build -``` - -For GitLab Pages, this `job` has a specific name, called `pages`, -which tells the Runner you want that task to deploy your website -with GitLab Pages: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -``` - -### The `public` directory - -We also need to tell Jekyll where do you want the website to build, -and GitLab Pages will only consider files in a directory called `public`. -To do that with Jekyll, we need to add a flag specifying the -[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the -built website: `jekyll build -d public`. Of course, we need -to tell this to our Runner: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public -``` - -### Artifacts - -We also need to tell the Runner that this _job_ generates -_artifacts_, which is the site built by Jekyll. -Where are these artifacts stored? In the `public` directory: - -```yaml -pages: - script: - - gem install jekyll - - jekyll build -d public - artifacts: - paths: - - public -``` - -The script above would be enough to build your Jekyll -site with GitLab Pages. But, from Jekyll 3.4.0 on, its default -template originated by `jekyll new project` requires -[Bundler](http://bundler.io/) to install Jekyll dependencies -and the default theme. To adjust our script to meet these new -requirements, we only need to install and build Jekyll with Bundler: - -```yaml -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -That's it! A `.gitlab-ci.yml` with the content above would deploy -your Jekyll 3.4.0 site with GitLab Pages. This is the minimum -configuration for our example. On the steps below, we'll refine -the script by adding extra options to our GitLab CI. - -### Image - -At this point, you probably ask yourself: "okay, but to install Jekyll -I need Ruby. Where is Ruby on that script?". The answer is simple: the -first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a -[Docker](https://www.docker.com/) image specifying what do you need in -your container to run that script: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public -``` - -In this case, you're telling the Runner to pull this image, which -contains Ruby 2.3 as part of its file system. When you don't specify -this image in your configuration, the Runner will use a default -image, which is Ruby 2.1. - -If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll -need to specify which image you want to use, and this image should -contain NodeJS as part of its file system. E.g., for a -[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. - ->**Note:** -We're not trying to explain what a Docker image is, -we just need to introduce the concept with a minimum viable -explanation. To know more about Docker images, please visit -their website or take a look at a -[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. - -Let's go a little further. - -### Branching - -If you use GitLab as a version control platform, you will have your -branching strategy to work on your project. Meaning, you will have -other branches in your project, but you'll want only pushes to the -default branch (usually `master`) to be deployed to your website. -To do that, we need to add another line to our CI, telling the Runner -to only perform that _job_ called `pages` on the `master` branch `only`: - -```yaml -image: ruby:2.3 - -pages: - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -### Stages - -Another interesting concept to keep in mind are build stages. -Your web app can pass through a lot of tests and other tasks -until it's deployed to staging or production environments. -There are three default stages on GitLab CI: build, test, -and deploy. To specify which stage your _job_ is running, -simply add another line to your CI: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master -``` - -You might ask yourself: "why should I bother with stages -at all?" Well, let's say you want to be able to test your -script and check the built site before deploying your site -to production. You want to run the test exactly as your -script will do when you push to `master`. It's simple, -let's add another task (_job_) to our CI, telling it to -test every push to other branches, `except` the `master` branch: - -```yaml -image: ruby:2.3 - -pages: - stage: deploy - script: - - bundle install - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle install - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -The `test` job is running on the stage `test`, Jekyll -will build the site in a directory called `test`, and -this job will affect all the branches except `master`. - -The best benefit of applying _stages_ to different -_jobs_ is that every job in the same stage builds in -parallel. So, if your web app needs more than one test -before being deployed, you can run all your test at the -same time, it's not necessary to wait one test to finish -to run the other. Of course, this is just a brief -introduction of GitLab CI and GitLab Runner, which are -tools much more powerful than that. This is what you -need to be able to create and tweak your builds for -your GitLab Pages site. - -### Before Script - -To avoid running the same script multiple times across -your _jobs_, you can add the parameter `before_script`, -in which you specify which commands you want to run for -every single _job_. In our example, notice that we run -`bundle install` for both jobs, `pages` and `test`. -We don't need to repeat it: - -```yaml -image: ruby:2.3 - -before_script: - - bundle install - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -### Caching Dependencies - -If you want to cache the installation files for your -projects dependencies, for building faster, you can -use the parameter `cache`. For this example, we'll -cache Jekyll dependencies in a `vendor` directory -when we run `bundle install`: - -```yaml -image: ruby:2.3 - -cache: - paths: - - vendor/ - -before_script: - - bundle install --path vendor - -pages: - stage: deploy - script: - - bundle exec jekyll build -d public - artifacts: - paths: - - public - only: - - master - -test: - stage: test - script: - - bundle exec jekyll build -d test - artifacts: - paths: - - test - except: - - master -``` - -For this specific case, we need to exclude `/vendor` -from Jekyll `_config.yml` file, otherwise Jekyll will -understand it as a regular directory to build -together with the site: - -```yml -exclude: - - vendor -``` - -There we go! Now our GitLab CI not only builds our website, -but also **continuously test** pushes to feature-branches, -**caches** dependencies installed with Bundler, and -**continuously deploy** every push to the `master` branch. - -## Advanced GitLab CI for GitLab Pages - -What you can do with GitLab CI is pretty much up to your -creativity. Once you get used to it, you start creating -awesome scripts that automate most of tasks you'd do -manually in the past. Read through the -[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) -to understand how to go even further on your scripts. - -- On this blog post, understand the concept of -[using GitLab CI `environments` to deploy your -web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). -- On this post, learn [how to run jobs sequentially, -in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) -- On this blog post, we go through the process of -[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) -to deploy this website you're looking at, docs.gitlab.com. -- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). - -||| -|:--|--:| -|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|| +This document was moved to [another location](../user/project/pages/getting_started_part_three.md). diff --git a/doc/pages/getting_started_part_two.md b/doc/pages/getting_started_part_two.md index 07dd24122c4..a58affec73d 100644 --- a/doc/pages/getting_started_part_two.md +++ b/doc/pages/getting_started_part_two.md @@ -1,152 +1 @@ -# GitLab Pages from A to Z: Part 2 - -> Type: user guide -> -> Level: beginner - -- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ -- **Part 2: Quick Start Guide - Setting Up GitLab Pages** -- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ - ----- - -## Setting up GitLab Pages - -For a complete step-by-step tutorial, please read the -blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain -what do you need and why do you need them. - -## What you need to get started - -1. A project -1. A configuration file (`.gitlab-ci.yml`) to deploy your site -1. A specific `job` called `pages` in the configuration file -that will make GitLab aware that you are deploying a GitLab Pages website - -Optional Features: - -1. A custom domain or subdomain -1. A DNS pointing your (sub)domain to your Pages site - 1. **Optional**: an SSL/TLS certificate so your custom - domain is accessible under HTTPS. - -## Project - -Your GitLab Pages project is a regular project created the -same way you do for the other ones. To get started with GitLab Pages, you have two ways: - -- Fork one of the templates from Page Examples, or -- Create a new project from scratch - -Let's go over both options. - -### Fork a project to get started from - -To make things easy for you, we've created this -[group](https://gitlab.com/pages) of default projects -containing the most popular SSGs templates. - -Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've -created for the steps below. - -1. Choose your SSG template -1. Fork a project from the [Pages group](https://gitlab.com/pages) -1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** - - ![remove fork relashionship](img/remove_fork_relashionship.png) - -1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** -1. Trigger a build (push a change to any file) -1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** - -To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: - -- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** -- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. - -> **Notes:** -> ->1. Why do I need to remove the fork relationship? -> -> Unless you want to contribute to the original project, -you won't need it connected to the upstream. A -[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) -is useful for submitting merge requests to the upstream. -> -> 2. Why do I need to enable Shared Runners? -> -> Shared Runners will run the script set by your GitLab CI -configuration file. They're enabled by default to new projects, -but not to forks. - -### Create a project from scratch - -1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, -click **New project**, and name it considering the -[practical examples](getting_started_part_one.md#practical-examples). -1. Clone it to your local computer, add your website -files to your project, add, commit and push to GitLab. -1. From the your **Project**'s page, click **Set up CI**: - - ![setup GitLab CI](img/setup_ci.png) - -1. Choose one of the templates from the dropbox menu. -Pick up the template corresponding to the SSG you're using (or plain HTML). - - ![gitlab-ci templates](img/choose_ci_template.png) - -Once you have both site files and `.gitlab-ci.yml` in your project's -root, GitLab CI will build your site and deploy it with Pages. -Once the first build passes, you see your site is live by -navigating to your **Project**'s **Settings** > **Pages**, -where you'll find its default URL. - -> **Notes:** -> -> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, -if you don't find yours among the templates, you'll need -to configure your own `.gitlab-ci.yml`. Do do that, please -read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among -the [example projects](https://gitlab.com/pages). If you set -up a new one, please -[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) -to our examples. -> -> - The second step _"Clone it to your local computer"_, can be done -differently, achieving the same results: instead of cloning the bare -repository to you local computer and moving your site files into it, -you can run `git init` in your local website directory, add the -remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, -then add, commit, and push. - -### URLs and Baseurls - -Every Static Site Generator (SSG) default configuration expects -to find your website under a (sub)domain (`example.com`), not -in a subdirectory of that domain (`example.com/subdir`). Therefore, -whenever you publish a project website (`namespace.gitlab.io/project-name`), -you'll have to look for this configuration (base URL) on your SSG's -documentation and set it up to reflect this pattern. - -For example, for a Jekyll site, the `baseurl` is defined in the Jekyll -configuration file, `_config.yml`. If your website URL is -`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: - -```yaml -baseurl: "/blog" -``` - -On the contrary, if you deploy your website after forking one of -our [default examples](https://gitlab.com/pages), the baseurl will -already be configured this way, as all examples there are project -websites. If you decide to make yours a user or group website, you'll -have to remove this configuration from your project. For the Jekyll -example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: - -```yaml -baseurl: "" -``` - -||| -|:--|--:| -|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_three.md)| +This document was moved to [another location](../user/project/pages/getting_started_part_two.md). diff --git a/doc/pages/img/dns_cname_record_example.png b/doc/pages/img/dns_cname_record_example.png Binary files differdeleted file mode 100644 index d64a843a283..00000000000 --- a/doc/pages/img/dns_cname_record_example.png +++ /dev/null diff --git a/doc/pages/img/remove_fork_relashionship.png b/doc/pages/img/remove_fork_relashionship.png Binary files differdeleted file mode 100644 index f5b5e543f21..00000000000 --- a/doc/pages/img/remove_fork_relashionship.png +++ /dev/null diff --git a/doc/pages/img/setup_ci.png b/doc/pages/img/setup_ci.png Binary files differdeleted file mode 100644 index 7ce0431f4d4..00000000000 --- a/doc/pages/img/setup_ci.png +++ /dev/null diff --git a/doc/pages/index.md b/doc/pages/index.md deleted file mode 100644 index a6f928cc243..00000000000 --- a/doc/pages/index.md +++ /dev/null @@ -1,49 +0,0 @@ -# All you need to know about GitLab Pages - -With GitLab Pages you can create static websites for your GitLab projects, -groups, or user accounts. You can use any static website generator: Jekyll, -Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains -as you like and bring your own TLS certificate to secure them. - -Here's some info we have gathered to get you started. - -## General info - -- [Product webpage](https://pages.gitlab.io) -- [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) -- [Pages group - templates](https://gitlab.com/pages) - -## Getting started - -- GitLab Pages from A to Z - - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) - - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) -- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide -- Secure GitLab Pages custom domain with SSL/TLS certificates - - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) - - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -- Static Site Generators - Blog posts series - - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) - - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) - -## Video tutorials - -- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) -- [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) - -## Advanced use - -- Blog Posts: - - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) - - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) - - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) - - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) - -## Specific documentation - -- [User docs](../user/project/pages/index.md) -- [Admin docs](../administration/pages/index.md) diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png Binary files differindex 3c5ff5ee317..f52acf4ef3b 100644 --- a/doc/user/project/integrations/img/mattermost_configuration.png +++ b/doc/user/project/integrations/img/mattermost_configuration.png diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png Binary files differindex fc8e58e686b..527824fc3eb 100644 --- a/doc/user/project/integrations/img/slack_configuration.png +++ b/doc/user/project/integrations/img/slack_configuration.png diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 09ba9994d3a..cfb0931273d 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - Wiki page -Bellow each of these event checkboxes, you will have an input field to insert -which Mattermost channel you want to send that event message, with `#town-square` -being the default. The hash sign is optional. +Below each of these event checkboxes, you have an input field to enter +which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional). At the end, fill in your Mattermost details: | Field | Description | | ----- | ----------- | -| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… | | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | - +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | ![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md new file mode 100644 index 00000000000..6aefe5dbded --- /dev/null +++ b/doc/user/project/integrations/mock_ci.md @@ -0,0 +1,13 @@ +# Mock CI Service + +**NB: This service is only listed if you are in a development environment!** + +To setup the mock CI service server, respond to the following endpoints + +- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json` + - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }` + - If the service returns a 404, it is interpreted as `pending` +- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}` + - Just where the build is linked to, doesn't matter if implemented + +For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 57a9492044b..f27f9a726fc 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered: - Push - Issue +- Confidential issue - Merge request - Note - Tag push - Build +- Pipeline - Wiki page -Bellow each of these event checkboxes, you will have an input field to insert -which Slack channel you want to send that event message, with `#general` -being the default. Enter your preferred channel **without** the hash sign (`#`). +Below each of these event checkboxes, you have an input field to enter +which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`). At the end, fill in your Slack details: | Field | Description | | ----- | ----------- | | **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | -| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | +| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | +| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | After you are all done, click **Save changes** for the changes to take effect. diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md new file mode 100644 index 00000000000..c5b1aa4b654 --- /dev/null +++ b/doc/user/project/pages/getting_started_part_one.md @@ -0,0 +1,266 @@ +# GitLab Pages from A to Z: Part 1 + +- **Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates** +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ + +---- + +This is a comprehensive guide, made for those who want to +publish a website with GitLab Pages but aren't familiar with +the entire process involved. + +To **enable** GitLab Pages for GitLab CE (Community Edition) +and GitLab EE (Enterprise Edition), please read the +[admin documentation](https://docs.gitlab.com/ce/administration/pages/index.html), +and/or watch this [video tutorial](https://youtu.be/dD8c7WNcc6s). + +>**Note:** +For this guide, we assume you already have GitLab Pages +server up and running for your GitLab instance. + +## What you need to know before getting started + +Before we begin, let's understand a few concepts first. + +### Static sites + +GitLab Pages only supports static websites, meaning, +your output files must be HTML, CSS, and JavaScript only. + +To create your static site, you can either hardcode in HTML, +CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/) +to simplify your code and build the static site for you, +which is highly recommendable and much faster than hardcoding. + +--- + +- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) +- Understand [how modern Static Site Generators work](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site +- You can use [any SSG with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- Fork an [example project](https://gitlab.com/pages) to build your website based upon + +### GitLab Pages domain + +If you set up a GitLab Pages project on GitLab.com, +it will automatically be accessible under a +[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). +The `namespace` is defined by your username on GitLab.com, +or the group name you created this project under. + +>**Note:** +If you use your own GitLab instance to deploy your +site with GitLab Pages, check with your sysadmin what's your +Pages wildcard domain. This guide is valid for any GitLab instance, +you just need to replace Pages wildcard domain on GitLab.com +(`*.gitlab.io`) with your own. + +#### Practical examples + +**Project Websites:** + +- You created a project called `blog` under your username `john`, +therefore your project URL is `https://gitlab.com/john/blog/`. +Once you enable GitLab Pages for this project, and build your site, +it will be available under `https://john.gitlab.io/blog/`. +- You created a group for all your websites called `websites`, +and a project within this group is called `blog`. Your project +URL is `https://gitlab.com/websites/blog/`. Once you enable +GitLab Pages for this project, the site will live under +`https://websites.gitlab.io/blog/`. + +**User and Group Websites:** + +- Under your username, `john`, you created a project called +`john.gitlab.io`. Your project URL will be `https://gitlab.com/john/john.gitlab.io`. +Once you enable GitLab Pages for your project, your website +will be published under `https://john.gitlab.io`. +- Under your group `websites`, you created a project called +`websites.gitlab.io`. your project's URL will be `https://gitlab.com/websites/websites.gitlab.io`. Once you enable GitLab Pages for your project, +your website will be published under `https://websites.gitlab.io`. + +**General example:** + +- On GitLab.com, a project site will always be available under +`https://namespace.gitlab.io/project-name` +- On GitLab.com, a user or group website will be available under +`https://namespace.gitlab.io/` +- On your GitLab instance, replace `gitlab.io` above with your +Pages server domain. Ask your sysadmin for this information. + +### DNS Records + +A Domain Name System (DNS) web service routes visitors to websites +by translating domain names (such as `www.example.com`) into the +numeric IP addresses (such as `192.0.2.1`) that computers use to +connect to each other. + +A DNS record is created to point a (sub)domain to a certain location, +which can be an IP address or another domain. In case you want to use +GitLab Pages with your own (sub)domain, you need to access your domain's +registrar control panel to add a DNS record pointing it back to your +GitLab Pages site. + +Note that **how to** add DNS records depends on which server your domain +is hosted on. Every control panel has its own place to do it. If you are +not an admin of your domain, and don't have access to your registrar, +you'll need to ask for the technical support of your hosting service +to do it for you. + +To help you out, we've gathered some instructions on how to do that +for the most popular hosting services: + +- [Amazon](http://docs.aws.amazon.com/gettingstarted/latest/swh/getting-started-configure-route53.html) +- [Bluehost](https://my.bluehost.com/cgi/help/559) +- [CloudFlare](https://support.cloudflare.com/hc/en-us/articles/200169096-How-do-I-add-A-records-) +- [cPanel](https://documentation.cpanel.net/display/ALD/Edit+DNS+Zone) +- [DreamHost](https://help.dreamhost.com/hc/en-us/articles/215414867-How-do-I-add-custom-DNS-records-) +- [Go Daddy](https://www.godaddy.com/help/add-an-a-record-19238) +- [Hostgator](http://support.hostgator.com/articles/changing-dns-records) +- [Inmotion hosting](https://my.bluehost.com/cgi/help/559) +- [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) +- [Microsoft](https://msdn.microsoft.com/en-us/library/bb727018.aspx) + +If your hosting service is not listed above, you can just try to +search the web for "how to add dns record on <my hosting service>". + +#### DNS A record + +In case you want to point a root domain (`example.com`) to your +GitLab Pages site, deployed to `namespace.gitlab.io`, you need to +log into your domain's admin control panel and add a DNS `A` record +pointing your domain to Pages' server IP address. For projects on +GitLab.com, this IP is `104.208.235.32`. For projects leaving in +other GitLab instances (CE or EE), please contact your sysadmin +asking for this information (which IP address is Pages server +running on your instance). + +**Practical Example:** + +![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) + +#### DNS CNAME record + +In case you want to point a subdomain (`hello-world.example.com`) +to your GitLab Pages site initially deployed to `namespace.gitlab.io`, +you need to log into your domain's admin control panel and add a DNS +`CNAME` record pointing your subdomain to your website URL +(`namespace.gitlab.io`) address. + +Notice that, despite it's a user or project website, the `CNAME` +should point to your Pages domain (`namespace.gitlab.io`), +without any `/project-name`. + +**Practical Example:** + +![DNS CNAME record pointing to GitLab.com project](img/dns_cname_record_example.png) + +#### TL;DR + +| From | DNS Record | To | +| ---- | ---------- | -- | +| domain.com | A | 104.208.235.32 | +| subdomain.domain.com | CNAME | namespace.gitlab.io | + +> **Notes**: +> +> - **Do not** use a CNAME record if you want to point your +`domain.com` to your GitLab Pages site. Use an `A` record instead. +> - **Do not** add any special chars after the default Pages +domain. E.g., **do not** point your `subdomain.domain.com` to +`namespace.gitlab.io.` or `namespace.gitlab.io/`. + +### SSL/TLS Certificates + +Every GitLab Pages project on GitLab.com will be available under +HTTPS for the default Pages domain (`*.gitlab.io`). Once you set +up your Pages project with your custom (sub)domain, if you want +it secured by HTTPS, you will have to issue a certificate for that +(sub)domain and install it on your project. + +>**Note:** +Certificates are NOT required to add to your custom +(sub)domain on your GitLab Pages project, though they are +highly recommendable. + +The importance of having any website securely served under HTTPS +is explained on the introductory section of the blog post +[Secure GitLab Pages with StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/#https-a-quick-overview). + +The reason why certificates are so important is that they encrypt +the connection between the **client** (you, me, your visitors) +and the **server** (where you site lives), through a keychain of +authentications and validations. + +### Issuing Certificates + +GitLab Pages accepts [PEM](https://support.quovadisglobal.com/kb/a37/what-is-pem-format.aspx) certificates issued by +[Certificate Authorities (CA)](https://en.wikipedia.org/wiki/Certificate_authority) +and self-signed certificates. Of course, +[you'd rather issue a certificate than generate a self-signed](https://en.wikipedia.org/wiki/Self-signed_certificate), +for security reasons and for having browsers trusting your +site's certificate. + +There are several different kinds of certificates, each one +with certain security level. A static personal website will +not require the same security level as an online banking web app, +for instance. There are a couple Certificate Authorities that +offer free certificates, aiming to make the internet more secure +to everyone. The most popular is [Let's Encrypt](https://letsencrypt.org/), +which issues certificates trusted by most of browsers, it's open +source, and free to use. Please read through this tutorial to +understand [how to secure your GitLab Pages website with Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/). + +With the same popularity, there are [certificates issued by CloudFlare](https://www.cloudflare.com/ssl/), +which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-free-cdn-and-you/). +Their certs are valid up to 15 years. Read through the tutorial on +[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/). + +### Adding certificates to your project + +Regardless the CA you choose, the steps to add your certificate to +your Pages project are the same. + +#### What do you need + +1. A PEM certificate +1. An intermediate certificate +1. A public key + +![Pages project - adding certificates](img/add_certificate_to_pages.png) + +These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**. + +#### What's what? + +- A PEM certificate is the certificate generated by the CA, +which needs to be added to the field **Certificate (PEM)**. +- An [intermediate certificate](https://en.wikipedia.org/wiki/Intermediate_certificate_authority) (aka "root certificate") is +the part of the encryption keychain that identifies the CA. +Usually it's combined with the PEM certificate, but there are +some cases in which you need to add them manually. +[CloudFlare certs](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) +are one of these cases. +- A public key is an encrypted key which validates +your PEM against your domain. + +#### Now what? + +Now that you hopefully understand why you need all +of this, it's simple: + +- Your PEM certificate needs to be added to the first field +- If your certificate is missing its intermediate, copy +and paste the root certificate (usually available from your CA website) +and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/), +just jumping a line between them. +- Copy your public key and paste it in the last field + +>**Note:** +**Do not** open certificates or encryption keys in +regular text editors. Always use code editors (such as +Sublime Text, Atom, Dreamweaver, Brackets, etc). + +||| +|:--|--:| +||[**Part 2: Quick start guide - Setting up GitLab Pages →**](getting_started_part_two.md)| diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md new file mode 100644 index 00000000000..ef47abef3a0 --- /dev/null +++ b/doc/user/project/pages/getting_started_part_three.md @@ -0,0 +1,383 @@ +# GitLab Pages from A to Z: Part 3 + +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- _[Part 2: Quick Start Guide - Setting Up GitLab Pages](getting_started_part_two.md)_ +- **Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages** + +--- + +## Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages + +[GitLab CI](https://about.gitlab.com/gitlab-ci/) serves +numerous purposes, to build, test, and deploy your app +from GitLab through +[Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) +methods. You will need it to build your website with GitLab Pages, +and deploy it to the Pages server. + +What this file actually does is telling the +[GitLab Runner](https://docs.gitlab.com/runner/) to run scripts +as you would do from the command line. The Runner acts as your +terminal. GitLab CI tells the Runner which commands to run. +Both are built-in in GitLab, and you don't need to set up +anything for them to work. + +Explaining [every detail of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +and GitLab Runner is out of the scope of this guide, but we'll +need to understand just a few things to be able to write our own +`.gitlab-ci.yml` or tweak an existing one. It's an +[Yaml](http://docs.ansible.com/ansible/YAMLSyntax.html) file, +with its own syntax. You can always check your CI syntax with +the [GitLab CI Lint Tool](https://gitlab.com/ci/lint). + +**Practical Example:** + +Let's consider you have a [Jekyll](https://jekyllrb.com/) site. +To build it locally, you would open your terminal, and run `jekyll build`. +Of course, before building it, you had to install Jekyll in your computer. +For that, you had to open your terminal and run `gem install jekyll`. +Right? GitLab CI + GitLab Runner do the same thing. But you need to +write in the `.gitlab-ci.yml` the script you want to run so +GitLab Runner will do it for you. It looks more complicated then it +is. What you need to tell the Runner: + +``` +$ gem install jekyll +$ jekyll build +``` + +### Script + +To transpose this script to Yaml, it would be like this: + +```yaml +script: + - gem install jekyll + - jekyll build +``` + +### Job + +So far so good. Now, each `script`, in GitLab is organized by +a `job`, which is a bunch of scripts and settings you want to +apply to that specific task. + +```yaml +job: + script: + - gem install jekyll + - jekyll build +``` + +For GitLab Pages, this `job` has a specific name, called `pages`, +which tells the Runner you want that task to deploy your website +with GitLab Pages: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build +``` + +### The `public` directory + +We also need to tell Jekyll where do you want the website to build, +and GitLab Pages will only consider files in a directory called `public`. +To do that with Jekyll, we need to add a flag specifying the +[destination (`-d`)](https://jekyllrb.com/docs/usage/) of the +built website: `jekyll build -d public`. Of course, we need +to tell this to our Runner: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public +``` + +### Artifacts + +We also need to tell the Runner that this _job_ generates +_artifacts_, which is the site built by Jekyll. +Where are these artifacts stored? In the `public` directory: + +```yaml +pages: + script: + - gem install jekyll + - jekyll build -d public + artifacts: + paths: + - public +``` + +The script above would be enough to build your Jekyll +site with GitLab Pages. But, from Jekyll 3.4.0 on, its default +template originated by `jekyll new project` requires +[Bundler](http://bundler.io/) to install Jekyll dependencies +and the default theme. To adjust our script to meet these new +requirements, we only need to install and build Jekyll with Bundler: + +```yaml +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +That's it! A `.gitlab-ci.yml` with the content above would deploy +your Jekyll 3.4.0 site with GitLab Pages. This is the minimum +configuration for our example. On the steps below, we'll refine +the script by adding extra options to our GitLab CI. + +### Image + +At this point, you probably ask yourself: "okay, but to install Jekyll +I need Ruby. Where is Ruby on that script?". The answer is simple: the +first thing GitLab Runner will look for in your `.gitlab-ci.yml` is a +[Docker](https://www.docker.com/) image specifying what do you need in +your container to run that script: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public +``` + +In this case, you're telling the Runner to pull this image, which +contains Ruby 2.3 as part of its file system. When you don't specify +this image in your configuration, the Runner will use a default +image, which is Ruby 2.1. + +If your SSG needs [NodeJS](https://nodejs.org/) to build, you'll +need to specify which image you want to use, and this image should +contain NodeJS as part of its file system. E.g., for a +[Hexo](https://gitlab.com/pages/hexo) site, you can use `image: node:4.2.2`. + +>**Note:** +We're not trying to explain what a Docker image is, +we just need to introduce the concept with a minimum viable +explanation. To know more about Docker images, please visit +their website or take a look at a +[summarized explanation](http://paislee.io/how-to-automate-docker-deployments/) here. + +Let's go a little further. + +### Branching + +If you use GitLab as a version control platform, you will have your +branching strategy to work on your project. Meaning, you will have +other branches in your project, but you'll want only pushes to the +default branch (usually `master`) to be deployed to your website. +To do that, we need to add another line to our CI, telling the Runner +to only perform that _job_ called `pages` on the `master` branch `only`: + +```yaml +image: ruby:2.3 + +pages: + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +### Stages + +Another interesting concept to keep in mind are build stages. +Your web app can pass through a lot of tests and other tasks +until it's deployed to staging or production environments. +There are three default stages on GitLab CI: build, test, +and deploy. To specify which stage your _job_ is running, +simply add another line to your CI: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master +``` + +You might ask yourself: "why should I bother with stages +at all?" Well, let's say you want to be able to test your +script and check the built site before deploying your site +to production. You want to run the test exactly as your +script will do when you push to `master`. It's simple, +let's add another task (_job_) to our CI, telling it to +test every push to other branches, `except` the `master` branch: + +```yaml +image: ruby:2.3 + +pages: + stage: deploy + script: + - bundle install + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle install + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +The `test` job is running on the stage `test`, Jekyll +will build the site in a directory called `test`, and +this job will affect all the branches except `master`. + +The best benefit of applying _stages_ to different +_jobs_ is that every job in the same stage builds in +parallel. So, if your web app needs more than one test +before being deployed, you can run all your test at the +same time, it's not necessary to wait one test to finish +to run the other. Of course, this is just a brief +introduction of GitLab CI and GitLab Runner, which are +tools much more powerful than that. This is what you +need to be able to create and tweak your builds for +your GitLab Pages site. + +### Before Script + +To avoid running the same script multiple times across +your _jobs_, you can add the parameter `before_script`, +in which you specify which commands you want to run for +every single _job_. In our example, notice that we run +`bundle install` for both jobs, `pages` and `test`. +We don't need to repeat it: + +```yaml +image: ruby:2.3 + +before_script: + - bundle install + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +### Caching Dependencies + +If you want to cache the installation files for your +projects dependencies, for building faster, you can +use the parameter `cache`. For this example, we'll +cache Jekyll dependencies in a `vendor` directory +when we run `bundle install`: + +```yaml +image: ruby:2.3 + +cache: + paths: + - vendor/ + +before_script: + - bundle install --path vendor + +pages: + stage: deploy + script: + - bundle exec jekyll build -d public + artifacts: + paths: + - public + only: + - master + +test: + stage: test + script: + - bundle exec jekyll build -d test + artifacts: + paths: + - test + except: + - master +``` + +For this specific case, we need to exclude `/vendor` +from Jekyll `_config.yml` file, otherwise Jekyll will +understand it as a regular directory to build +together with the site: + +```yml +exclude: + - vendor +``` + +There we go! Now our GitLab CI not only builds our website, +but also **continuously test** pushes to feature-branches, +**caches** dependencies installed with Bundler, and +**continuously deploy** every push to the `master` branch. + +## Advanced GitLab CI for GitLab Pages + +What you can do with GitLab CI is pretty much up to your +creativity. Once you get used to it, you start creating +awesome scripts that automate most of tasks you'd do +manually in the past. Read through the +[documentation of GitLab CI](https://docs.gitlab.com/ce/ci/yaml/README.html) +to understand how to go even further on your scripts. + +- On this blog post, understand the concept of +[using GitLab CI `environments` to deploy your +web app to staging and production](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/). +- On this post, learn [how to run jobs sequentially, +in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- On this blog post, we go through the process of +[pulling specific directories from different projects](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) +to deploy this website you're looking at, docs.gitlab.com. +- On this blog post, we teach you [how to use GitLab Pages to produce a code coverage report](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/). + +||| +|:--|--:| +|[**← Part 2: Quick start guide - Setting up GitLab Pages**](getting_started_part_two.md)|| diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md new file mode 100644 index 00000000000..07dd24122c4 --- /dev/null +++ b/doc/user/project/pages/getting_started_part_two.md @@ -0,0 +1,152 @@ +# GitLab Pages from A to Z: Part 2 + +> Type: user guide +> +> Level: beginner + +- _[Part 1: Static Sites, Domains, DNS Records, and SSL/TLS Certificates](getting_started_part_one.md)_ +- **Part 2: Quick Start Guide - Setting Up GitLab Pages** +- _[Part 3: Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md)_ + +---- + +## Setting up GitLab Pages + +For a complete step-by-step tutorial, please read the +blog post [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). The following sections will explain +what do you need and why do you need them. + +## What you need to get started + +1. A project +1. A configuration file (`.gitlab-ci.yml`) to deploy your site +1. A specific `job` called `pages` in the configuration file +that will make GitLab aware that you are deploying a GitLab Pages website + +Optional Features: + +1. A custom domain or subdomain +1. A DNS pointing your (sub)domain to your Pages site + 1. **Optional**: an SSL/TLS certificate so your custom + domain is accessible under HTTPS. + +## Project + +Your GitLab Pages project is a regular project created the +same way you do for the other ones. To get started with GitLab Pages, you have two ways: + +- Fork one of the templates from Page Examples, or +- Create a new project from scratch + +Let's go over both options. + +### Fork a project to get started from + +To make things easy for you, we've created this +[group](https://gitlab.com/pages) of default projects +containing the most popular SSGs templates. + +Watch the [video tutorial](https://youtu.be/TWqh9MtT4Bg) we've +created for the steps below. + +1. Choose your SSG template +1. Fork a project from the [Pages group](https://gitlab.com/pages) +1. Remove the fork relationship by navigating to your **Project**'s **Settings** > **Edit Project** + + ![remove fork relashionship](img/remove_fork_relashionship.png) + +1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** +1. Trigger a build (push a change to any file) +1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** + +To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: + +- Rename it to `namespace.gitlab.io`: navigate to **Project**'s **Settings** > **Edit Project** > **Rename repository** +- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file. + +> **Notes:** +> +>1. Why do I need to remove the fork relationship? +> +> Unless you want to contribute to the original project, +you won't need it connected to the upstream. A +[fork](https://about.gitlab.com/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/#fork) +is useful for submitting merge requests to the upstream. +> +> 2. Why do I need to enable Shared Runners? +> +> Shared Runners will run the script set by your GitLab CI +configuration file. They're enabled by default to new projects, +but not to forks. + +### Create a project from scratch + +1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**, +click **New project**, and name it considering the +[practical examples](getting_started_part_one.md#practical-examples). +1. Clone it to your local computer, add your website +files to your project, add, commit and push to GitLab. +1. From the your **Project**'s page, click **Set up CI**: + + ![setup GitLab CI](img/setup_ci.png) + +1. Choose one of the templates from the dropbox menu. +Pick up the template corresponding to the SSG you're using (or plain HTML). + + ![gitlab-ci templates](img/choose_ci_template.png) + +Once you have both site files and `.gitlab-ci.yml` in your project's +root, GitLab CI will build your site and deploy it with Pages. +Once the first build passes, you see your site is live by +navigating to your **Project**'s **Settings** > **Pages**, +where you'll find its default URL. + +> **Notes:** +> +> - GitLab Pages [supports any SSG](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but, +if you don't find yours among the templates, you'll need +to configure your own `.gitlab-ci.yml`. Do do that, please +read through the article [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md). New SSGs are very welcome among +the [example projects](https://gitlab.com/pages). If you set +up a new one, please +[contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md) +to our examples. +> +> - The second step _"Clone it to your local computer"_, can be done +differently, achieving the same results: instead of cloning the bare +repository to you local computer and moving your site files into it, +you can run `git init` in your local website directory, add the +remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`, +then add, commit, and push. + +### URLs and Baseurls + +Every Static Site Generator (SSG) default configuration expects +to find your website under a (sub)domain (`example.com`), not +in a subdirectory of that domain (`example.com/subdir`). Therefore, +whenever you publish a project website (`namespace.gitlab.io/project-name`), +you'll have to look for this configuration (base URL) on your SSG's +documentation and set it up to reflect this pattern. + +For example, for a Jekyll site, the `baseurl` is defined in the Jekyll +configuration file, `_config.yml`. If your website URL is +`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`: + +```yaml +baseurl: "/blog" +``` + +On the contrary, if you deploy your website after forking one of +our [default examples](https://gitlab.com/pages), the baseurl will +already be configured this way, as all examples there are project +websites. If you decide to make yours a user or group website, you'll +have to remove this configuration from your project. For the Jekyll +example we've just mentioned, you'd have to change Jekyll's `_config.yml` to: + +```yaml +baseurl: "" +``` + +||| +|:--|--:| +|[**← Part 1: Static sites, domains, DNS records, and SSL/TLS certificates**](getting_started_part_one.md)|[**Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages →**](getting_started_part_three.md)| diff --git a/doc/pages/img/add_certificate_to_pages.png b/doc/user/project/pages/img/add_certificate_to_pages.png Binary files differindex d92a981dc60..d92a981dc60 100644 --- a/doc/pages/img/add_certificate_to_pages.png +++ b/doc/user/project/pages/img/add_certificate_to_pages.png diff --git a/doc/pages/img/choose_ci_template.png b/doc/user/project/pages/img/choose_ci_template.png Binary files differindex 0697542abc8..0697542abc8 100644 --- a/doc/pages/img/choose_ci_template.png +++ b/doc/user/project/pages/img/choose_ci_template.png diff --git a/doc/pages/img/dns_a_record_example.png b/doc/user/project/pages/img/dns_a_record_example.png Binary files differindex b923730388a..b923730388a 100644 --- a/doc/pages/img/dns_a_record_example.png +++ b/doc/user/project/pages/img/dns_a_record_example.png diff --git a/doc/user/project/pages/img/dns_cname_record_example.png b/doc/user/project/pages/img/dns_cname_record_example.png Binary files differnew file mode 100644 index 00000000000..43d1a838544 --- /dev/null +++ b/doc/user/project/pages/img/dns_cname_record_example.png diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png Binary files differindex a936d8e5dbd..be47f9d2a44 100644 --- a/doc/user/project/pages/img/pages_create_project.png +++ b/doc/user/project/pages/img/pages_create_project.png diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png Binary files differindex 3f615d3757d..2f1a19ae424 100644 --- a/doc/user/project/pages/img/pages_create_user_page.png +++ b/doc/user/project/pages/img/pages_create_user_page.png diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png Binary files differindex 8d34f3b7f38..274e98fde4d 100644 --- a/doc/user/project/pages/img/pages_dns_details.png +++ b/doc/user/project/pages/img/pages_dns_details.png diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png Binary files differindex 2bc7cee07a6..6bc92db6b41 100644 --- a/doc/user/project/pages/img/pages_multiple_domains.png +++ b/doc/user/project/pages/img/pages_multiple_domains.png diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png Binary files differindex c3640133bb2..cd59defa006 100644 --- a/doc/user/project/pages/img/pages_new_domain_button.png +++ b/doc/user/project/pages/img/pages_new_domain_button.png diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png Binary files differindex adbfb654877..b064310380e 100644 --- a/doc/user/project/pages/img/pages_remove.png +++ b/doc/user/project/pages/img/pages_remove.png diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png Binary files differindex 06d85ab1971..dc431ea3fef 100644 --- a/doc/user/project/pages/img/pages_upload_cert.png +++ b/doc/user/project/pages/img/pages_upload_cert.png diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relashionship.png Binary files differnew file mode 100644 index 00000000000..67c45491f08 --- /dev/null +++ b/doc/user/project/pages/img/remove_fork_relashionship.png diff --git a/doc/user/project/pages/img/setup_ci.png b/doc/user/project/pages/img/setup_ci.png Binary files differnew file mode 100644 index 00000000000..214c1cc668f --- /dev/null +++ b/doc/user/project/pages/img/setup_ci.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 276fbd26835..dbb9d9ad9c4 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -1,449 +1,49 @@ -# GitLab Pages +# All you need to know about GitLab Pages -> **Notes:** -> - This feature was [introduced][ee-80] in GitLab EE 8.3. -> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. -> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. -> - This document is about the user guide. To learn how to enable GitLab Pages -> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). +With GitLab Pages you can create static websites for your GitLab projects, +groups, or user accounts. You can use any static website generator: Jekyll, +Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains +as you like and bring your own TLS certificate to secure them. -With GitLab Pages you can host for free your static websites on GitLab. -Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can -deploy static pages for your individual projects, your user or your group. +Here's some info we have gathered to get you started. -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific -information, if you are using GitLab.com to host your website. +## General info -Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials). +- [Product webpage](https://pages.gitlab.io) +- [We're bringing GitLab Pages to CE](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- [Pages group - templates](https://gitlab.com/pages) -## Getting started with GitLab Pages +## Getting started -> **Note:** -> In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. +- GitLab Pages from A to Z + - [Part 1: Static sites, domains, DNS records, and SSL/TLS certificates](getting_started_part_one.md) + - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) + - [Part 3: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_three.md) +- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide +- Secure GitLab Pages custom domain with SSL/TLS certificates + - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) + - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) + - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) +- Static Site Generators - Blog posts series + - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) + - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) + - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) +- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) -In general there are two types of pages one might create: +## Video tutorials -- Pages per user (`username.example.io`) or per group (`groupname.example.io`) -- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) +- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) +- [How to Enable GitLab Pages for GitLab CE and EE](https://youtu.be/dD8c7WNcc6s) -In GitLab, usernames and groupnames are unique and we often refer to them -as namespaces. There can be only one namespace in a GitLab instance. Below you -can see the connection between the type of GitLab Pages, what the project name -that is created on GitLab looks like and the website URL it will be ultimately -be served on. +## Advanced use -| Type of GitLab Pages | The name of the project created in GitLab | Website URL | -| -------------------- | ------------ | ----------- | -| User pages | `username.example.io` | `http(s)://username.example.io` | -| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | -| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | -| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| +- Blog Posts: + - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) + - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) + - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) + - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) -> **Warning:** -> There are some known [limitations](#limitations) regarding namespaces served -> under the general domain name and HTTPS. Make sure to read that section. +## Specific documentation -### GitLab Pages requirements - -In brief, this is what you need to upload your website in GitLab Pages: - -1. Find out the general domain name that is used for GitLab Pages - (ask your administrator). This is very important, so you should first make - sure you get that right. -1. Create a project -1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory - of your repository with a specific job named [`pages`][pages] -1. Set up a GitLab Runner to build your website - -> **Note:** -If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab -administrator, you should be able to use them instead of bringing your own. - -### User or group Pages - -For user and group pages, the name of the project should be specific to the -username or groupname and the general domain name that is used for GitLab Pages. -Head over your GitLab instance that supports GitLab Pages and create a -repository named `username.example.io`, where `username` is your username on -GitLab. If the first part of the project name doesn't match exactly your -username, it won’t work, so make sure to get it right. - -To create a group page, the steps are the same like when creating a website for -users. Just make sure that you are creating the project within the group's -namespace. - -![Create a user-based pages project](img/pages_create_user_page.png) - ---- - -After you push some static content to your repository and GitLab Runner uploads -the artifacts to GitLab CI, you will be able to access your website under -`http(s)://username.example.io`. Keep reading to find out how. - ->**Note:** -If your username/groupname contains a dot, for example `foo.bar`, you will not -be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). - -### Project Pages - -GitLab Pages for projects can be created by both user and group accounts. -The steps to create a project page for a user or a group are identical: - -1. Create a new project -1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory - of your repository with a specific job named [`pages`][pages]. -1. Set up a GitLab Runner to build your website - -A user's project will be served under `http(s)://username.example.io/projectname` -whereas a group's project under `http(s)://groupname.example.io/projectname`. - -## Quick Start - -Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on -[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork]. - -See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages. - -### Explore the contents of `.gitlab-ci.yml` - -The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that -gives you absolute control over the build process. You can actually watch your -website being built live by following the CI job traces. - -> **Note:** -> Before reading this section, make sure you familiarize yourself with GitLab CI -> and the specific syntax of[`.gitlab-ci.yml`][yaml] by -> following our [quick start guide]. - -To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the -rules below: - -1. A special job named [`pages`][pages] must be defined -1. Any static content which will be served by GitLab Pages must be placed under - a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -In its simplest form, `.gitlab-ci.yml` looks like: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public -``` - -When the Runner reaches to build the `pages` job, it executes whatever is -defined in the `script` parameter and if the job completes with a non-zero -exit status, it then uploads the `public/` directory to GitLab Pages. - -The `public/` directory should contain all the static content of your website. -Depending on how you plan to publish your website, the steps defined in the -[`script` parameter](../../../ci/yaml/README.md#script) may differ. - -Be aware that Pages are by default branch/tag agnostic and their deployment -relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the -`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to whatever branch or tag, the Pages will be -overwritten. In the example below, we limit the Pages to be deployed whenever -a commit is pushed only on the `master` branch: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public - only: - - master -``` - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. And since all these parameters were all under a `pages` -job, the contents of the `public` directory will be served by GitLab Pages. - -#### How `.gitlab-ci.yml` looks like when the static content is in your repository - -Supposedly your repository contained the following files: - -``` -├── index.html -├── css -│ └── main.css -└── js - └── main.js -``` - -Then the `.gitlab-ci.yml` example below simply moves all files from the root -directory of the project to the `public/` directory. The `.public` workaround -is so `cp` doesn't also copy `public/` to itself in an infinite loop: - -```yaml -pages: - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -#### How `.gitlab-ci.yml` looks like when using a static generator - -In general, GitLab Pages support any kind of [static site generator][staticgen], -since `.gitlab-ci.yml` can be configured to run any possible command. - -In the root directory of your Git repository, place the source files of your -favorite static generator. Then provide a `.gitlab-ci.yml` file which is -specific to your static generator. - -The example below, uses [Jekyll] to build the static site: - -```yaml -image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 - -pages: # the build job must be named pages - script: - - gem install jekyll # we install jekyll - - jekyll build -d public/ # we tell jekyll to build the site for us - artifacts: - paths: - - public # this is where the site will live and the Runner uploads it in GitLab - only: - - master # this script is only affecting the master branch -``` - -Here, we used the Docker executor and in the first line we specified the base -image against which our jobs will run. - -You have to make sure that the generated static files are ultimately placed -under the `public` directory, that's why in the `script` section we run the -`jekyll` command that jobs the website and puts all content in the `public/` -directory. Depending on the static generator of your choice, this command will -differ. Search in the documentation of the static generator you will use if -there is an option to explicitly set the output directory. If there is not -such an option, you can always add one more line under `script` to rename the -resulting directory in `public/`. - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. - ---- - -See the [jekyll example project][pages-jekyll] to better understand how this -works. - -For a list of Pages projects, see the [example projects](#example-projects) to -get you started. - -#### How to set up GitLab Pages in a repository where there's also actual code - -Remember that GitLab Pages are by default branch/tag agnostic and their -deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit -the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to a branch that will be used specifically for -your pages. - -That way, you can have your project's code in the `master` branch and use an -orphan branch (let's name it `pages`) that will host your static generator site. - -You can create a new empty branch like this: - -```bash -git checkout --orphan pages -``` - -The first commit made on this new branch will have no parents and it will be -the root of a new history totally disconnected from all the other branches and -commits. Push the source files of your static generator in the `pages` branch. - -Below is a copy of `.gitlab-ci.yml` where the most significant line is the last -one, specifying to execute everything in the `pages` branch: - -``` -image: ruby:2.1 - -pages: - script: - - gem install jekyll - - jekyll build -d public/ - artifacts: - paths: - - public - only: - - pages -``` - -See an example that has different files in the [`master` branch][jekyll-master] -and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which -also includes `.gitlab-ci.yml`. - -[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master -[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages - -## Next steps - -So you have successfully deployed your website, congratulations! Let's check -what more you can do with GitLab Pages. - -### Example projects - -Below is a list of example projects for GitLab Pages with a plain HTML website -or various static site generators. Contributions are very welcome. - -- [Plain HTML](https://gitlab.com/pages/plain-html) -- [Jekyll](https://gitlab.com/pages/jekyll) -- [Hugo](https://gitlab.com/pages/hugo) -- [Middleman](https://gitlab.com/pages/middleman) -- [Hexo](https://gitlab.com/pages/hexo) -- [Brunch](https://gitlab.com/pages/brunch) -- [Metalsmith](https://gitlab.com/pages/metalsmith) -- [Harp](https://gitlab.com/pages/harp) - -Visit the GitLab Pages group for a full list of example projects: -<https://gitlab.com/groups/pages>. - -### Add a custom domain to your Pages website - -If this setting is enabled by your GitLab administrator, you should be able to -see the **New Domain** button when visiting your project's settings through the -gear icon in the top right and then navigating to **Pages**. - -![New domain button](img/pages_new_domain_button.png) - ---- - -You can add multiple domains pointing to your website hosted under GitLab. -Once the domain is added, you can see it listed under the **Domains** section. - -![Pages multiple domains](img/pages_multiple_domains.png) - ---- - -As a last step, you need to configure your DNS and add a CNAME pointing to your -user/group page. Click on the **Details** button of a domain for further -instructions. - -![Pages DNS details](img/pages_dns_details.png) - ---- - ->**Note:** -Currently there is support only for custom domains on per-project basis. That -means that if you add a custom domain (`example.com`) for your user website -(`username.example.io`), a project that is served under `username.example.io/foo`, -will not be accessible under `example.com/foo`. - -### Secure your custom domain website with TLS - -When you add a new custom domain, you also have the chance to add a TLS -certificate. If this setting is enabled by your GitLab administrator, you -should be able to see the option to upload the public certificate and the -private key when adding a new domain. - -![Pages upload cert](img/pages_upload_cert.png) - -### Custom error codes pages - -You can provide your own 403 and 404 error pages by creating the `403.html` and -`404.html` files respectively in the root directory of the `public/` directory -that will be included in the artifacts. Usually this is the root directory of -your project, but that may differ depending on your static generator -configuration. - -If the case of `404.html`, there are different scenarios. For example: - -- If you use project Pages (served under `/projectname/`) and try to access - `/projectname/non/exsiting_file`, GitLab Pages will try to serve first - `/projectname/404.html`, and then `/404.html`. -- If you use user/group Pages (served under `/`) and try to access - `/non/existing_file` GitLab Pages will try to serve `/404.html`. -- If you use a custom domain and try to access `/non/existing_file`, GitLab - Pages will try to serve only `/404.html`. - -### Remove the contents of your pages - -If you ever feel the need to purge your Pages content, you can do so by going -to your project's settings through the gear icon in the top right, and then -navigating to **Pages**. Hit the **Remove pages** button and your Pages website -will be deleted. Simple as that. - -![Remove pages](img/pages_remove.png) - -## GitLab Pages on GitLab.com - -If you are using GitLab.com to host your website, then: - -- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. -- Custom domains and TLS support are enabled. -- Shared runners are enabled by default, provided for free and can be used to - build your website. If you want you can still bring your own Runner. - -The rest of the guide still applies. - -## Limitations - -When using Pages under the general domain of a GitLab instance (`*.example.io`), -you _cannot_ use HTTPS with sub-subdomains. That means that if your -username/groupname contains a dot, for example `foo.bar`, the domain -`https://foo.bar.example.io` will _not_ work. This is a limitation of the -[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you -don't redirect HTTP to HTTPS. - -[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" - -## Redirects in GitLab Pages - -Since you cannot use any custom server configuration files, like `.htaccess` or -any `.conf` file for that matter, if you want to redirect a web page to another -location, you can use the [HTTP meta refresh tag][metarefresh]. - -Some static site generators provide plugins for that functionality so that you -don't have to create and edit HTML files manually. For example, Jekyll has the -[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). - -## Frequently Asked Questions - -### Can I download my generated pages? - -Sure. All you need to do is download the artifacts archive from the job page. - -### Can I use GitLab Pages if my project is private? - -Yes. GitLab Pages don't care whether you set your project's visibility level -to private, internal or public. - -### Do I need to create a user/group website before creating a project website? - -No, you don't. You can create your project first and it will be accessed under -`http(s)://namespace.example.io/projectname`. - -## Known issues - -For a list of known issues, visit GitLab's [public issue tracker]. - ---- - -[jekyll]: http://jekyllrb.com/ -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages -[gitlab ci]: https://about.gitlab.com/gitlab-ci -[gitlab runner]: https://docs.gitlab.com/runner -[pages]: ../../../ci/yaml/README.md#pages -[yaml]: ../../../ci/yaml/README.md -[staticgen]: https://www.staticgen.com/ -[pages-jekyll]: https://gitlab.com/pages/jekyll -[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh -[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages -[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 -[quick start guide]: ../../../ci/quick_start/README.md -[pages-index-guide]: ../../../pages/index.md -[pages-quick]: ../../../pages/getting_started_part_one.md -[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg +- [User docs](introduction.md) +- [Admin docs](../../../administration/pages/index.md) diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md new file mode 100644 index 00000000000..deaceabb7c5 --- /dev/null +++ b/doc/user/project/pages/introduction.md @@ -0,0 +1,447 @@ +# GitLab Pages + +> **Notes:** +> - This feature was [introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. +> - This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). + +With GitLab Pages you can host for free your static websites on GitLab. +Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can +deploy static pages for your individual projects, your user or your group. + +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific +information, if you are using GitLab.com to host your website. + +Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials). + +## Getting started with GitLab Pages + +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + +In general there are two types of pages one might create: + +- Pages per user (`username.example.io`) or per group (`groupname.example.io`) +- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) + +In GitLab, usernames and groupnames are unique and we often refer to them +as namespaces. There can be only one namespace in a GitLab instance. Below you +can see the connection between the type of GitLab Pages, what the project name +that is created on GitLab looks like and the website URL it will be ultimately +be served on. + +| Type of GitLab Pages | The name of the project created in GitLab | Website URL | +| -------------------- | ------------ | ----------- | +| User pages | `username.example.io` | `http(s)://username.example.io` | +| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | +| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | +| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| + +> **Warning:** +> There are some known [limitations](#limitations) regarding namespaces served +> under the general domain name and HTTPS. Make sure to read that section. + +### GitLab Pages requirements + +In brief, this is what you need to upload your website in GitLab Pages: + +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project +1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory + of your repository with a specific job named [`pages`][pages] +1. Set up a GitLab Runner to build your website + +> **Note:** +If [shared runners](../../../ci/runners/README.md) are enabled by your GitLab +administrator, you should be able to use them instead of bringing your own. + +### User or group Pages + +For user and group pages, the name of the project should be specific to the +username or groupname and the general domain name that is used for GitLab Pages. +Head over your GitLab instance that supports GitLab Pages and create a +repository named `username.example.io`, where `username` is your username on +GitLab. If the first part of the project name doesn't match exactly your +username, it won’t work, so make sure to get it right. + +To create a group page, the steps are the same like when creating a website for +users. Just make sure that you are creating the project within the group's +namespace. + +![Create a user-based pages project](img/pages_create_user_page.png) + +--- + +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. + +>**Note:** +If your username/groupname contains a dot, for example `foo.bar`, you will not +be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). + +### Project Pages + +GitLab Pages for projects can be created by both user and group accounts. +The steps to create a project page for a user or a group are identical: + +1. Create a new project +1. Push a [`.gitlab-ci.yml` file][yaml] in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website + +A user's project will be served under `http(s)://username.example.io/projectname` +whereas a group's project under `http(s)://groupname.example.io/projectname`. + +## Quick Start + +Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on +[how to publish a website with GitLab Pages on GitLab.com from a forked project][video-pages-fork]. + +See also [All you Need to Know About GitLab Pages][pages-index-guide] for a list with all the resources we have for GitLab Pages. + +### Explore the contents of `.gitlab-ci.yml` + +The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that +gives you absolute control over the build process. You can actually watch your +website being built live by following the CI job traces. + +> **Note:** +> Before reading this section, make sure you familiarize yourself with GitLab CI +> and the specific syntax of[`.gitlab-ci.yml`][yaml] by +> following our [quick start guide]. + +To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the +rules below: + +1. A special job named [`pages`][pages] must be defined +1. Any static content which will be served by GitLab Pages must be placed under + a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +In its simplest form, `.gitlab-ci.yml` looks like: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public +``` + +When the Runner reaches to build the `pages` job, it executes whatever is +defined in the `script` parameter and if the job completes with a non-zero +exit status, it then uploads the `public/` directory to GitLab Pages. + +The `public/` directory should contain all the static content of your website. +Depending on how you plan to publish your website, the steps defined in the +[`script` parameter](../../../ci/yaml/README.md#script) may differ. + +Be aware that Pages are by default branch/tag agnostic and their deployment +relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the +`pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to whatever branch or tag, the Pages will be +overwritten. In the example below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public + only: + - master +``` + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. And since all these parameters were all under a `pages` +job, the contents of the `public` directory will be served by GitLab Pages. + +#### How `.gitlab-ci.yml` looks like when the static content is in your repository + +Supposedly your repository contained the following files: + +``` +├── index.html +├── css +│ └── main.css +└── js + └── main.js +``` + +Then the `.gitlab-ci.yml` example below simply moves all files from the root +directory of the project to the `public/` directory. The `.public` workaround +is so `cp` doesn't also copy `public/` to itself in an infinite loop: + +```yaml +pages: + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +#### How `.gitlab-ci.yml` looks like when using a static generator + +In general, GitLab Pages support any kind of [static site generator][staticgen], +since `.gitlab-ci.yml` can be configured to run any possible command. + +In the root directory of your Git repository, place the source files of your +favorite static generator. Then provide a `.gitlab-ci.yml` file which is +specific to your static generator. + +The example below, uses [Jekyll] to build the static site: + +```yaml +image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 + +pages: # the build job must be named pages + script: + - gem install jekyll # we install jekyll + - jekyll build -d public/ # we tell jekyll to build the site for us + artifacts: + paths: + - public # this is where the site will live and the Runner uploads it in GitLab + only: + - master # this script is only affecting the master branch +``` + +Here, we used the Docker executor and in the first line we specified the base +image against which our jobs will run. + +You have to make sure that the generated static files are ultimately placed +under the `public` directory, that's why in the `script` section we run the +`jekyll` command that jobs the website and puts all content in the `public/` +directory. Depending on the static generator of your choice, this command will +differ. Search in the documentation of the static generator you will use if +there is an option to explicitly set the output directory. If there is not +such an option, you can always add one more line under `script` to rename the +resulting directory in `public/`. + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. + +--- + +See the [jekyll example project][pages-jekyll] to better understand how this +works. + +For a list of Pages projects, see the [example projects](#example-projects) to +get you started. + +#### How to set up GitLab Pages in a repository where there's also actual code + +Remember that GitLab Pages are by default branch/tag agnostic and their +deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit +the `pages` job with the [`only` parameter](../../../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to a branch that will be used specifically for +your pages. + +That way, you can have your project's code in the `master` branch and use an +orphan branch (let's name it `pages`) that will host your static generator site. + +You can create a new empty branch like this: + +```bash +git checkout --orphan pages +``` + +The first commit made on this new branch will have no parents and it will be +the root of a new history totally disconnected from all the other branches and +commits. Push the source files of your static generator in the `pages` branch. + +Below is a copy of `.gitlab-ci.yml` where the most significant line is the last +one, specifying to execute everything in the `pages` branch: + +``` +image: ruby:2.1 + +pages: + script: + - gem install jekyll + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - pages +``` + +See an example that has different files in the [`master` branch][jekyll-master] +and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which +also includes `.gitlab-ci.yml`. + +[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages + +## Next steps + +So you have successfully deployed your website, congratulations! Let's check +what more you can do with GitLab Pages. + +### Example projects + +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. + +- [Plain HTML](https://gitlab.com/pages/plain-html) +- [Jekyll](https://gitlab.com/pages/jekyll) +- [Hugo](https://gitlab.com/pages/hugo) +- [Middleman](https://gitlab.com/pages/middleman) +- [Hexo](https://gitlab.com/pages/hexo) +- [Brunch](https://gitlab.com/pages/brunch) +- [Metalsmith](https://gitlab.com/pages/metalsmith) +- [Harp](https://gitlab.com/pages/harp) + +Visit the GitLab Pages group for a full list of example projects: +<https://gitlab.com/groups/pages>. + +### Add a custom domain to your Pages website + +If this setting is enabled by your GitLab administrator, you should be able to +see the **New Domain** button when visiting your project's settings through the +gear icon in the top right and then navigating to **Pages**. + +![New domain button](img/pages_new_domain_button.png) + +--- + +You can add multiple domains pointing to your website hosted under GitLab. +Once the domain is added, you can see it listed under the **Domains** section. + +![Pages multiple domains](img/pages_multiple_domains.png) + +--- + +As a last step, you need to configure your DNS and add a CNAME pointing to your +user/group page. Click on the **Details** button of a domain for further +instructions. + +![Pages DNS details](img/pages_dns_details.png) + +--- + +>**Note:** +Currently there is support only for custom domains on per-project basis. That +means that if you add a custom domain (`example.com`) for your user website +(`username.example.io`), a project that is served under `username.example.io/foo`, +will not be accessible under `example.com/foo`. + +### Secure your custom domain website with TLS + +When you add a new custom domain, you also have the chance to add a TLS +certificate. If this setting is enabled by your GitLab administrator, you +should be able to see the option to upload the public certificate and the +private key when adding a new domain. + +![Pages upload cert](img/pages_upload_cert.png) + +### Custom error codes pages + +You can provide your own 403 and 404 error pages by creating the `403.html` and +`404.html` files respectively in the root directory of the `public/` directory +that will be included in the artifacts. Usually this is the root directory of +your project, but that may differ depending on your static generator +configuration. + +If the case of `404.html`, there are different scenarios. For example: + +- If you use project Pages (served under `/projectname/`) and try to access + `/projectname/non/exsiting_file`, GitLab Pages will try to serve first + `/projectname/404.html`, and then `/404.html`. +- If you use user/group Pages (served under `/`) and try to access + `/non/existing_file` GitLab Pages will try to serve `/404.html`. +- If you use a custom domain and try to access `/non/existing_file`, GitLab + Pages will try to serve only `/404.html`. + +### Remove the contents of your pages + +If you ever feel the need to purge your Pages content, you can do so by going +to your project's settings through the gear icon in the top right, and then +navigating to **Pages**. Hit the **Remove pages** button and your Pages website +will be deleted. Simple as that. + +![Remove pages](img/pages_remove.png) + +## GitLab Pages on GitLab.com + +If you are using GitLab.com to host your website, then: + +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. +- Custom domains and TLS support are enabled. +- Shared runners are enabled by default, provided for free and can be used to + build your website. If you want you can still bring your own Runner. + +The rest of the guide still applies. + +## Limitations + +When using Pages under the general domain of a GitLab instance (`*.example.io`), +you _cannot_ use HTTPS with sub-subdomains. That means that if your +username/groupname contains a dot, for example `foo.bar`, the domain +`https://foo.bar.example.io` will _not_ work. This is a limitation of the +[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you +don't redirect HTTP to HTTPS. + +[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" + +## Redirects in GitLab Pages + +Since you cannot use any custom server configuration files, like `.htaccess` or +any `.conf` file for that matter, if you want to redirect a web page to another +location, you can use the [HTTP meta refresh tag][metarefresh]. + +Some static site generators provide plugins for that functionality so that you +don't have to create and edit HTML files manually. For example, Jekyll has the +[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). + +## Frequently Asked Questions + +### Can I download my generated pages? + +Sure. All you need to do is download the artifacts archive from the job page. + +### Can I use GitLab Pages if my project is private? + +Yes. GitLab Pages don't care whether you set your project's visibility level +to private, internal or public. + +### Do I need to create a user/group website before creating a project website? + +No, you don't. You can create your project first and it will be accessed under +`http(s)://namespace.example.io/projectname`. + +## Known issues + +For a list of known issues, visit GitLab's [public issue tracker]. + +[jekyll]: http://jekyllrb.com/ +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[gitlab ci]: https://about.gitlab.com/gitlab-ci +[gitlab runner]: https://docs.gitlab.com/runner/ +[pages]: ../../../ci/yaml/README.md#pages +[yaml]: ../../../ci/yaml/README.md +[staticgen]: https://www.staticgen.com/ +[pages-jekyll]: https://gitlab.com/pages/jekyll +[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh +[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=pages +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 +[quick start guide]: ../../../ci/quick_start/README.md +[pages-index-guide]: index.md +[pages-quick]: getting_started_part_one.md +[video-pages-fork]: https://youtu.be/TWqh9MtT4Bg diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 8c5020bee37..9cc45065eb2 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` +>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git + LFS will not be working properly for people cloning the project. + ```bash + git add .gitattributes + ``` + Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 92061dac7f4..b1d5e4a7acb 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -11,6 +11,7 @@ Feature: Dashboard And I visit dashboard page Scenario: I should see projects list + Then I should see "New Project" link Then I should see "Shop" project link Then I should see "Shop" project CI status diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature index 88fef674c0c..c57376aecff 100644 --- a/features/project/commits/branches.feature +++ b/features/project/commits/branches.feature @@ -13,6 +13,7 @@ Feature: Project Commits Branches Given I visit project protected branches page Then I should see "Shop" protected branches list + @javascript Scenario: I create a branch Given I visit project branches page And I click new branch link @@ -33,12 +34,7 @@ Feature: Project Commits Branches And I submit new branch form with invalid name Then I should see new an error that branch is invalid - Scenario: I create a branch with invalid reference - Given I visit project branches page - And I click new branch link - And I submit new branch form with invalid reference - Then I should see new an error that ref is invalid - + @javascript Scenario: I create a branch that already exists Given I visit project branches page And I click new branch link diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 2b4a5ab0864..7dc33ab5683 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps step 'I should see an http link to the repository' do project = Project.find_by(name: 'Community') - expect(page).to have_field('project_clone', with: project.http_url_to_repo) + expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user)) end step 'I should see an ssh link to the repository' do diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index 5f9b9e0445e..ccaf3237815 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'I submit new branch form' do fill_in 'branch_name', with: 'deploy_keys' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with invalid name' do fill_in 'branch_name', with: '1.0 stable' - fill_in 'ref', with: 'master' - click_button 'Create branch' - end - - step 'I submit new branch form with invalid reference' do - fill_in 'branch_name', with: 'foo' - fill_in 'ref', with: 'foo' + select_branch('master') click_button 'Create branch' end step 'I submit new branch form with branch that already exists' do fill_in 'branch_name', with: 'master' - fill_in 'ref', with: 'master' + select_branch('master') click_button 'Create branch' end @@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps expect(page).to have_content "can't contain spaces" end - step 'I should see new an error that ref is invalid' do - expect(page).to have_content 'Invalid reference name' - end - step 'I should see new an error that branch already exists' do expect(page).to have_content 'Branch already exists' end @@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step "I should not see branch 'improve/awesome'" do expect(page.all(visible: true)).not_to have_content 'improve/awesome' end + + def select_branch(branch_name) + click_button 'master' + + page.within '#new-branch-form .dropdown-menu' do + click_link branch_name + end + end end diff --git a/lib/api/api.rb b/lib/api/api.rb index dc732012a33..b27ac3f1d15 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,10 +5,13 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::AwardEmoji mount ::API::V3::Boards mount ::API::V3::Branches + mount ::API::V3::BroadcastMessages mount ::API::V3::Commits mount ::API::V3::DeployKeys + mount ::API::V3::Environments mount ::API::V3::Files mount ::API::V3::Groups mount ::API::V3::Issues @@ -21,12 +24,16 @@ module API mount ::API::V3::Projects mount ::API::V3::ProjectSnippets mount ::API::V3::Repositories + mount ::API::V3::Runners + mount ::API::V3::Services mount ::API::V3::Subscriptions mount ::API::V3::SystemHooks mount ::API::V3::Tags - mount ::API::V3::Todos mount ::API::V3::Templates + mount ::API::V3::Todos + mount ::API::V3::Triggers mount ::API::V3::Users + mount ::API::V3::Variables end before { allow_access_with_scope :api } @@ -91,6 +98,7 @@ module API mount ::API::Projects mount ::API::ProjectSnippets mount ::API::Repositories + mount ::API::Runner mount ::API::Runners mount ::API::Services mount ::API::Session diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 301271118d4..07a1bcdbe18 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -83,7 +83,6 @@ module API unauthorized! unless award.user == current_user || current_user.admin? award.destroy - present award, with: Entities::AwardEmoji end end end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index f4226e5a89d..b6843c1b6af 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -127,9 +127,7 @@ module API service = ::Boards::Lists::DestroyService.new(user_project, current_user) - if service.execute(list) - present list, with: Entities::List - else + unless service.execute(list) render_api_error!({ error: 'List could not be deleted!' }, 400) end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 34f136948c2..73a7e939627 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -124,11 +124,7 @@ module API result = DeleteBranchService.new(user_project, current_user). execute(params[:branch]) - if result[:status] == :success - { - branch: params[:branch] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 1217002bf8e..395c401203c 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -91,7 +91,7 @@ module API delete ':id' do message = find_message - present message.destroy, with: Entities::BroadcastMessage + message.destroy end end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9ce396d4660..fd03e92264d 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -52,13 +52,6 @@ module API attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch]) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 0e37f40a887..a99d9cadc8a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -618,6 +618,10 @@ module API end end + class RunnerRegistrationDetails < Grape::Entity + expose :id, :token + end + class BuildArtifactFile < Grape::Entity expose :filename, :size end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 1a7e68f0528..dbdf29a9640 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -79,7 +79,7 @@ module API environment = user_project.environments.find(params[:environment_id]) - present environment.destroy, with: Entities::Environment + environment.destroy end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 500f9d3c787..9c4e43d77cc 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -118,10 +118,7 @@ module API file_params = declared_params(include_missing: false) result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute - if result[:status] == :success - status(200) - commit_response(file_params) - else + if result[:status] != :success render_api_error!(result[:message], 400) end end diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb new file mode 100644 index 00000000000..119ca81b883 --- /dev/null +++ b/lib/api/helpers/runner.rb @@ -0,0 +1,23 @@ +module API + module Helpers + module Runner + def runner_registration_token_valid? + ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], + current_application_settings.runners_registration_token) + end + + def get_runner_version_from_params + return unless params['info'].present? + attributes_for_keys(%w(name version revision platform architecture), params['info']) + end + + def authenticate_runner! + forbidden! unless current_runner + end + + def current_runner + @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) + end + end + end +end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index d2955af3f95..59f0e7cb647 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,7 +1,7 @@ module API class Labels < Grape::API include PaginationParams - + before { authenticate! } params do @@ -56,7 +56,7 @@ module API label = user_project.labels.find_by(title: params[:name]) not_found!('Label') unless label - present label.destroy, with: Entities::Label, current_user: current_user, project: user_project + label.destroy end desc 'Update an existing label. At least one optional parameter is required.' do diff --git a/lib/api/members.rb b/lib/api/members.rb index 8360c007005..baf85e6075a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -55,7 +55,6 @@ module API authorize_admin_source!(source_type, source) member = source.members.find_by(user_id: params[:user_id]) - conflict!('Member already exists') if member member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) @@ -63,9 +62,6 @@ module API if member.persisted? && member.valid? present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -87,9 +83,6 @@ module API if member.update_attributes(declared_params(include_missing: false)) present member.user, with: Entities::Member, member: member else - # This is to ensure back-compatibility but 400 behavior should be used - # for all validation errors in 9.0! - render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_validation_error!(member) end end @@ -100,24 +93,10 @@ module API end delete ":id/members/:user_id" do source = find_source(source_type, params[:id]) + # Ensure that memeber exists + source.members.find_by!(user_id: params[:user_id]) - # This is to ensure back-compatibility but find_by! should be used - # in that casse in 9.0! - member = source.members.find_by(user_id: params[:user_id]) - - # This is to ensure back-compatibility but this should be removed in - # favor of find_by! in 9.0! - not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil? - - # This is to ensure back-compatibility but 204 behavior should be used - # for all DELETE endpoints in 9.0! - if member.nil? - { message: "Access revoked", id: params[:user_id].to_i } - else - ::Members::DestroyService.new(source, current_user, declared_params).execute - - present member.user, with: Entities::Member, member: member - end + ::Members::DestroyService.new(source, current_user, declared_params).execute end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f559a7f74a0..3b3e45cbd06 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -132,8 +132,6 @@ module API authorize! :admin_note, note ::Notes::DestroyService.new(user_project, current_user).execute(note) - - present note, with: Entities::Note end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index f7a28d7ad10..57a5f97dc7f 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -90,12 +90,9 @@ module API requires :hook_id, type: Integer, desc: 'The ID of the hook to delete' end delete ":id/hooks/:hook_id" do - begin - present user_project.hooks.destroy(params[:hook_id]), with: Entities::ProjectHook - rescue - # ProjectHook can raise Error if hook_id not found - not_found!("Error deleting hook #{params[:hook_id]}") - end + hook = user_project.hooks.find(params.delete(:hook_id)) + + hook.destroy end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index b89bddc7e29..b8a8cee0cea 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,8 +94,9 @@ module API success Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + optional :name, type: String, desc: 'The name of the project' optional :path, type: String, desc: 'The path of the repository' + at_least_one_of :name, :path use :optional_params use :create_params end @@ -353,7 +354,6 @@ module API not_found!('Group Link') unless link link.destroy - no_content! end desc 'Upload a file' diff --git a/lib/api/runner.rb b/lib/api/runner.rb new file mode 100644 index 00000000000..47858f1866b --- /dev/null +++ b/lib/api/runner.rb @@ -0,0 +1,52 @@ +module API + class Runner < Grape::API + helpers ::API::Helpers::Runner + + resource :runners do + desc 'Registers a new Runner' do + success Entities::RunnerRegistrationDetails + http_codes [[201, 'Runner was created'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: 'Registration token' + optional :description, type: String, desc: %q(Runner's description) + optional :info, type: Hash, desc: %q(Runner's metadata) + optional :locked, type: Boolean, desc: 'Should Runner be locked for current project' + optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' + optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) + end + post '/' do + attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list] + + runner = + if runner_registration_token_valid? + # Create shared runner. Requires admin access + Ci::Runner.create(attributes.merge(is_shared: true)) + elsif project = Project.find_by(runners_token: params[:token]) + # Create a specific runner for project. + project.runners.create(attributes) + end + + return forbidden! unless runner + + if runner.id + runner.update(get_runner_version_from_params) + present runner, with: Entities::RunnerRegistrationDetails + else + not_found! + end + end + + desc 'Deletes a registered Runner' do + http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + end + delete '/' do + authenticate_runner! + Ci::Runner.find_by_token(params[:token]).destroy + end + end + end +end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 252e59bfa58..2e41f16f8c6 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -78,9 +78,8 @@ module API delete ':id' do runner = get_runner(params[:id]) authenticate_delete_runner!(runner) - runner.destroy! - present runner, with: Entities::Runner + runner.destroy! end end @@ -136,8 +135,6 @@ module API forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 runner_project.destroy - - present runner, with: Entities::Runner end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1456fe4688b..79a5f27dc4d 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -563,7 +563,20 @@ module API SlackService, MattermostService, TeamcityService, - ].freeze + ] + + if Rails.env.development? + services['mock-ci'] = [ + { + required: true, + name: :mock_service_url, + type: String, + desc: 'URL to the mock service' + } + ] + + service_classes << MockCiService + end trigger_services = { 'mattermost-slash-commands' => [ @@ -641,9 +654,7 @@ module API hash.merge!(key => nil) end - if service.update_attributes(attrs.merge(active: false)) - true - else + unless service.update_attributes(attrs.merge(active: false)) render_api_error!('400 Bad Request', 400) end end diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index ac03fbd2a3d..0f86fdb3075 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -118,9 +118,10 @@ module API delete ':id' do snippet = snippets_for_current_user.find_by(id: params.delete(:id)) return not_found!('Snippet') unless snippet + authorize! :destroy_personal_snippet, snippet + snippet.destroy - no_content! end desc 'Get a raw snippet' do diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index d038a3fa828..ed7b23b474a 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -66,7 +66,7 @@ module API hook = SystemHook.find_by(id: params[:id]) not_found!('System hook') unless hook - present hook.destroy, with: Entities::Hook + hook.destroy end end end diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 86759ab882f..d31ef9de26b 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -66,11 +66,7 @@ module API result = ::Tags::DestroyService.new(user_project, current_user). execute(params[:tag_name]) - if result[:status] == :success - { - tag_name: params[:tag_name] - } - else + if result[:status] != :success render_api_error!(result[:message], result[:return_code]) end end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index ea0ad852633..b7c9c5f2b7f 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -93,8 +93,6 @@ module API return not_found!('Trigger') unless trigger trigger.destroy - - present trigger, with: Entities::Trigger end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 94b2b6653d2..7bb4b76f830 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -236,7 +236,7 @@ module API key = user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc 'Add an email address to a specified user. Available only for admins.' do @@ -422,7 +422,7 @@ module API key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key - present key.destroy, with: Entities::SSHKey + key.destroy end desc "Get the currently authenticated user's email addresses" do diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb new file mode 100644 index 00000000000..1e35283631f --- /dev/null +++ b/lib/api/v3/award_emoji.rb @@ -0,0 +1,59 @@ +module API + module V3 + class AwardEmoji < Grape::API + include PaginationParams + + before { authenticate! } + AWARDABLES = %w[issue merge_request snippet].freeze + + resource :projects do + AWARDABLES.each do |awardable_type| + awardable_string = awardable_type.pluralize + awardable_id_string = "#{awardable_type}_id" + + params do + requires :id, type: String, desc: 'The ID of a project' + requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" + end + + [":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", + ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"].each do |endpoint| + desc 'Delete a +awardables+ award emoji' do + detail 'This feature was introduced in 8.9' + success ::API::Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of an award emoji' + end + delete "#{endpoint}/:award_id" do + award = awardable.award_emoji.find(params[:award_id]) + + unauthorized! unless award.user == current_user || current_user.admin? + + present award.destroy, with: ::API::Entities::AwardEmoji + end + end + end + end + + helpers do + def awardable + @awardable ||= + begin + if params.include?(:note_id) + note_id = params.delete(:note_id) + + awardable.notes.find(note_id) + elsif params.include?(:issue_id) + user_project.issues.find(params[:issue_id]) + elsif params.include?(:merge_request_id) + user_project.merge_requests.find(params[:merge_request_id]) + else + user_project.snippets.find(params[:snippet_id]) + end + end + end + end + end + end +end diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb index 31d708bc2c8..b1c2a3c59f2 100644 --- a/lib/api/v3/boards.rb +++ b/lib/api/v3/boards.rb @@ -44,6 +44,27 @@ module API authorize!(:read_board, user_project) present board_lists, with: ::API::Entities::List end + + desc 'Delete a board list' do + detail 'This feature was introduced in 8.13' + success ::API::Entities::List + end + params do + requires :list_id, type: Integer, desc: 'The ID of a board list' + end + delete "/lists/:list_id" do + authorize!(:admin_list, user_project) + + list = board_lists.find(params[:list_id]) + + service = ::Boards::Lists::DestroyService.new(user_project, current_user) + + if service.execute(list) + present list, with: ::API::Entities::List + else + render_api_error!({ error: 'List could not be deleted!' }, 400) + end + end end end end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index 51eb566cf7d..699e41b5537 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -19,6 +19,26 @@ module API present branches, with: ::API::Entities::RepoBranch, project: user_project end + desc 'Delete a branch' + params do + requires :branch, type: String, desc: 'The name of the branch' + end + delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do + authorize_push_project + + result = DeleteBranchService.new(user_project, current_user). + execute(params[:branch]) + + if result[:status] == :success + status(200) + { + branch_name: params[:branch] + } + else + render_api_error!(result[:message], result[:return_code]) + end + end + desc 'Delete all merged branches' delete ":id/repository/merged_branches" do DeleteMergedBranchesService.new(user_project, current_user).async_execute diff --git a/lib/api/v3/broadcast_messages.rb b/lib/api/v3/broadcast_messages.rb new file mode 100644 index 00000000000..417e4ad0b26 --- /dev/null +++ b/lib/api/v3/broadcast_messages.rb @@ -0,0 +1,31 @@ +module API + module V3 + class BroadcastMessages < Grape::API + include PaginationParams + + before { authenticate! } + before { authenticated_as_admin! } + + resource :broadcast_messages do + helpers do + def find_message + BroadcastMessage.find(params[:id]) + end + end + + desc 'Delete a broadcast message' do + detail 'This feature was introduced in GitLab 8.12.' + success ::API::Entities::BroadcastMessage + end + params do + requires :id, type: Integer, desc: 'Broadcast message ID' + end + delete ':id' do + message = find_message + + present message.destroy, with: ::API::Entities::BroadcastMessage + end + end + end + end +end diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 126cc95fc3d..506204b3517 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -55,13 +55,6 @@ module API branch = attrs.delete(:branch_name) attrs.merge!(branch: branch, start_branch: branch, target_branch: branch) - attrs[:actions].map! do |action| - action[:action] = action[:action].to_sym - action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/') - action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/') - action - end - result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb new file mode 100644 index 00000000000..3effccfa708 --- /dev/null +++ b/lib/api/v3/environments.rb @@ -0,0 +1,29 @@ +module API + module V3 + class Environments < Grape::API + include PaginationParams + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The project ID' + end + resource :projects do + desc 'Deletes an existing environment' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + delete ':id/environments/:environment_id' do + authorize! :update_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + + present environment.destroy, with: ::API::Entities::Environment + end + end + end + end +end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index d0af09f0e1e..5d7dfabfcd6 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -226,6 +226,8 @@ module API not_found!('Issue') unless issue authorize!(:destroy_issue, issue) + + status(200) issue.destroy end end diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index 5c3261311bf..41f45d244e3 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -13,6 +13,21 @@ module API get ':id/labels' do present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project end + + desc 'Delete an existing label' do + success ::API::Entities::Label + end + params do + requires :name, type: String, desc: 'The name of the label to be deleted' + end + delete ':id/labels' do + authorize! :admin_label, user_project + + label = user_project.labels.find_by(title: params[:name]) + not_found!('Label') unless label + + present label.destroy, with: ::API::Entities::Label, current_user: current_user, project: user_project + end end end end diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb index 19f276d5484..3d4972afd9d 100644 --- a/lib/api/v3/members.rb +++ b/lib/api/v3/members.rb @@ -119,6 +119,7 @@ module API # This is to ensure back-compatibility but 204 behavior should be used # for all DELETE endpoints in 9.0! if member.nil? + status(200 ) { message: "Access revoked", id: params[:user_id].to_i } else ::Members::DestroyService.new(source, current_user, declared_params).execute diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 129f9d850e9..c6574a9104b 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -103,6 +103,8 @@ module API merge_request = find_project_merge_request(params[:merge_request_id]) authorize!(:destroy_merge_request, merge_request) + + status(200) merge_request.destroy end diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb index e03e941d30b..809ca4f37ba 100644 --- a/lib/api/v3/project_snippets.rb +++ b/lib/api/v3/project_snippets.rb @@ -121,6 +121,8 @@ module API authorize! :admin_project_snippet, snippet snippet.destroy + + status(200) end desc 'Get a raw project snippet' diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index c3821555452..881d52e4aa4 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -172,8 +172,9 @@ module API success ::API::Entities::Project end params do - requires :name, type: String, desc: 'The name of the project' + optional :name, type: String, desc: 'The name of the project' optional :path, type: String, desc: 'The path of the repository' + at_least_one_of :name, :path use :optional_params use :create_params end @@ -359,6 +360,8 @@ module API desc 'Remove a project' delete ":id" do authorize! :remove_project, user_project + + status(200) ::Projects::DestroyService.new(user_project, current_user, {}).async_execute end @@ -384,6 +387,7 @@ module API authorize! :remove_fork_project, user_project if user_project.forked? + status(200) user_project.forked_project_link.destroy else not_modified! diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb new file mode 100644 index 00000000000..8967141fe3d --- /dev/null +++ b/lib/api/v3/runners.rb @@ -0,0 +1,65 @@ +module API + module V3 + class Runners < Grape::API + include PaginationParams + + before { authenticate! } + + resource :runners do + desc 'Remove a runner' do + success ::API::Entities::Runner + end + params do + requires :id, type: Integer, desc: 'The ID of the runner' + end + delete ':id' do + runner = Ci::Runner.find(params[:id]) + not_found!('Runner') unless runner + + authenticate_delete_runner!(runner) + + status(200) + runner.destroy + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + before { authorize_admin_project } + + desc "Disable project's runner" do + success ::API::Entities::Runner + end + params do + requires :runner_id, type: Integer, desc: 'The ID of the runner' + end + delete ':id/runners/:runner_id' do + runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) + not_found!('Runner') unless runner_project + + runner = runner_project.runner + forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1 + + runner_project.destroy + + present runner, with: ::API::Entities::Runner + end + end + + helpers do + def authenticate_delete_runner!(runner) + return if current_user.is_admin? + forbidden!("Runner is shared") if runner.is_shared? + forbidden!("Runner associated with more than one project") if runner.projects.count > 1 + forbidden!("No access granted") unless user_can_access_runner?(runner) + end + + def user_can_access_runner?(runner) + current_user.ci_authorized_runners.exists?(runner.id) + end + end + end + end +end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb new file mode 100644 index 00000000000..af0a058f69b --- /dev/null +++ b/lib/api/v3/services.rb @@ -0,0 +1,573 @@ +module API + module V3 + class Services < Grape::API + services = { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Passord of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'builds-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :add_pusher, + type: Boolean, + desc: 'Add pusher to recipients list' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + } + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'gemnasium' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'Your personal API key on gemnasium.com' + }, + { + required: true, + name: :token, + type: String, + desc: "The project's slug on gemnasium.com" + } + ], + 'hipchat' => [ + { + required: true, + name: :token, + type: String, + desc: 'The room token' + }, + { + required: false, + name: :room, + type: String, + desc: 'The room name or ID' + }, + { + required: false, + name: :color, + type: String, + desc: 'The room color' + }, + { + required: false, + name: :notify, + type: Boolean, + desc: 'Enable notifications' + }, + { + required: false, + name: :api_version, + type: String, + desc: 'Leave blank for default (v2)' + }, + { + required: false, + name: :server, + type: String, + desc: 'Leave blank for default. https://hipchat.example.com' + } + ], + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The URL to the JIRA project which is being linked to this GitLab project, e.g., https://jira.example.com' + }, + { + required: true, + name: :project_key, + type: String, + desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ' + }, + { + required: false, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: Integer, + desc: '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`' + } + ], + + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + }, + ], + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Slack token' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_builds, + type: Boolean, + desc: 'Notify only broken builds' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Slack webhook. e.g. https://hooks.slack.com/services/...' + }, + { + required: false, + name: :new_issue_url, + type: String, + desc: 'The user name' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The channel name' + } + ], + 'mattermost' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...' + } + ], + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + } + + resource :projects do + before { authenticate! } + before { authorize_admin_project } + + helpers do + def service_attributes(service) + service.fields.inject([]) do |arr, hash| + arr << hash[:name].to_sym + end + end + end + + desc "Delete a service for project" + params do + requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + end + delete ":id/services/:service_slug" do + service = user_project.find_or_initialize_service(params[:service_slug].underscore) + + attrs = service_attributes(service).inject({}) do |hash, key| + hash.merge!(key => nil) + end + + if service.update_attributes(attrs.merge(active: false)) + status(200) + true + else + render_api_error!('400 Bad Request', 400) + end + end + end + end + end +end diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb index 391510b9ee0..5787c06fc12 100644 --- a/lib/api/v3/system_hooks.rb +++ b/lib/api/v3/system_hooks.rb @@ -13,6 +13,19 @@ module API get do present SystemHook.all, with: ::API::Entities::Hook end + + desc 'Delete a hook' do + success ::API::Entities::Hook + end + params do + requires :id, type: Integer, desc: 'The ID of the system hook' + end + delete ":id" do + hook = SystemHook.find_by(id: params[:id]) + not_found!('System hook') unless hook + + present hook.destroy, with: ::API::Entities::Hook + end end end end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb index 016e3d86932..6913720d9c5 100644 --- a/lib/api/v3/tags.rb +++ b/lib/api/v3/tags.rb @@ -14,6 +14,26 @@ module API tags = user_project.repository.tags.sort_by(&:name).reverse present tags, with: ::API::Entities::RepoTag, project: user_project end + + desc 'Delete a repository tag' + params do + requires :tag_name, type: String, desc: 'The name of the tag' + end + delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do + authorize_push_project + + result = ::Tags::DestroyService.new(user_project, current_user). + execute(params[:tag_name]) + + if result[:status] == :success + status(200) + { + tag_name: params[:tag_name] + } + else + render_api_error!(result[:message], result[:return_code]) + end + end end end end diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb index 4f9b5fe72a6..e60cb25e57b 100644 --- a/lib/api/v3/todos.rb +++ b/lib/api/v3/todos.rb @@ -19,6 +19,8 @@ module API desc 'Mark all todos as done' delete do + status(200) + todos = TodosFinder.new(current_user, params).execute TodoService.new.mark_todos_as_done(todos, current_user) end diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb new file mode 100644 index 00000000000..4051d4bca8d --- /dev/null +++ b/lib/api/v3/triggers.rb @@ -0,0 +1,30 @@ +module API + module V3 + class Triggers < Grape::API + include PaginationParams + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Delete a trigger' do + success ::API::Entities::Trigger + end + params do + requires :token, type: String, desc: 'The unique token of trigger' + end + delete ':id/triggers/:token' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find_by(token: params[:token].to_s) + return not_found!('Trigger') unless trigger + + trigger.destroy + + present trigger, with: ::API::Entities::Trigger + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 7838cdc46a7..14f54731730 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -92,6 +92,25 @@ module API present paginate(events), with: ::API::V3::Entities::Event end + + desc 'Delete an existing SSH key from a specified user. Available only for admins.' do + success ::API::Entities::SSHKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key_id, type: Integer, desc: 'The ID of the SSH key' + end + delete ':id/keys/:key_id' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + key = user.keys.find_by(id: params[:key_id]) + not_found!('Key') unless key + + present key.destroy, with: ::API::Entities::SSHKey + end end resource :user do @@ -111,6 +130,19 @@ module API get "emails" do present current_user.emails, with: ::API::Entities::Email end + + desc 'Delete an SSH key from the currently authenticated user' do + success ::API::Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the SSH key' + end + delete "keys/:key_id" do + key = current_user.keys.find_by(id: params[:key_id]) + not_found!('Key') unless key + + present key.destroy, with: ::API::Entities::SSHKey + end end end end diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb new file mode 100644 index 00000000000..0f55a14fb28 --- /dev/null +++ b/lib/api/v3/variables.rb @@ -0,0 +1,29 @@ +module API + module V3 + class Variables < Grape::API + include PaginationParams + + before { authenticate! } + before { authorize! :admin_build, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects do + desc 'Delete an existing variable from a project' do + success ::API::Entities::Variable + end + params do + requires :key, type: String, desc: 'The key of the variable' + end + delete ':id/variables/:key' do + variable = user_project.variables.find_by(key: params[:key]) + not_found!('Variable') unless variable + + present variable.destroy, with: ::API::Entities::Variable + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index f623b1dfe9f..77e5d54c225 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,5 +1,4 @@ module API - # Projects variables API class Variables < Grape::API include PaginationParams @@ -81,10 +80,9 @@ module API end delete ':id/variables/:key' do variable = user_project.variables.find_by(key: params[:key]) + not_found!('Variable') unless variable - return not_found!('Variable') unless variable - - present variable.destroy, with: Entities::Variable + variable.destroy end end end diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index f0fb6084a35..651b55523c0 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -8,11 +8,6 @@ module Banzai # of the anchor, and then replace the img with the link-wrapped version. def call doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| - div = doc.document.create_element( - 'div', - class: 'image-container' - ) - link = doc.document.create_element( 'a', class: 'no-attachment-icon', @@ -22,9 +17,7 @@ module Banzai link.children = img.clone - div.children = link - - img.replace(div) + img.replace(link) end doc diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 0e17ac24d5a..b51e76d93f2 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -217,6 +217,7 @@ module Ci build = Ci::Build.find_by_id(params[:id]) authenticate_build!(build) + status(200) build.erase_artifacts! end end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 2a611a67eaf..c1fd959ef14 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -8,6 +8,8 @@ module Ci end delete "delete" do authenticate_runner! + + status(200) Ci::Runner.find_by_token(params[:token]).destroy end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index b742d9e1e4b..e56eb0d3beb 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -93,163 +93,6 @@ module Gitlab commit_id: sha, ) end - - # Commit file in repository and return commit sha - # - # options should contain next structure: - # file: { - # content: 'Lorem ipsum...', - # path: 'documents/story.txt', - # update: true - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Wow such commit', - # branch: 'master', - # update_ref: false - # } - # - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def commit(repository, options, action = :add) - file = options[:file] - update = file[:update].nil? ? true : file[:update] - author = options[:author] - committer = options[:committer] - commit = options[:commit] - repo = repository.rugged - ref = commit[:branch] - update_ref = commit[:update_ref].nil? ? true : commit[:update_ref] - parents = [] - mode = 0o100644 - - unless ref.start_with?('refs/') - ref = 'refs/heads/' + ref - end - - path_name = Gitlab::Git::PathHelper.normalize_path(file[:path]) - # Abort if any invalid characters remain (e.g. ../foo) - raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if path_name.each_filename.to_a.include?('..') - - filename = path_name.to_s - index = repo.index - - unless repo.empty? - rugged_ref = repo.references[ref] - raise Gitlab::Git::Repository::InvalidRef.new("Invalid branch name") unless rugged_ref - last_commit = rugged_ref.target - index.read_tree(last_commit.tree) - parents = [last_commit] - end - - if action == :remove - index.remove(filename) - else - file_entry = index.get(filename) - - if action == :rename - old_path_name = Gitlab::Git::PathHelper.normalize_path(file[:previous_path]) - old_filename = old_path_name.to_s - file_entry = index.get(old_filename) - index.remove(old_filename) unless file_entry.blank? - end - - if file_entry - raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists; update not allowed") unless update - - # Preserve the current file mode if one is available - mode = file_entry[:mode] if file_entry[:mode] - end - - content = file[:content] - detect = CharlockHolmes::EncodingDetector.new.detect(content) if content - - unless detect && detect[:type] == :binary - # When writing to the repo directly as we are doing here, - # the `core.autocrlf` config isn't taken into account. - content.gsub!("\r\n", "\n") if repository.autocrlf - end - - oid = repo.write(content, :blob) - index.add(path: filename, oid: oid, mode: mode) - end - - opts = {} - opts[:tree] = index.write_tree(repo) - opts[:author] = author - opts[:committer] = committer - opts[:message] = commit[:message] - opts[:parents] = parents - opts[:update_ref] = ref if update_ref - - Rugged::Commit.create(repo, opts) - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - - # Remove file from repository and return commit sha - # - # options should contain next structure: - # file: { - # path: 'documents/story.txt' - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Remove FILENAME', - # branch: 'master' - # } - # - def remove(repository, options) - commit(repository, options, :remove) - end - - # Rename file from repository and return commit sha - # - # options should contain next structure: - # file: { - # previous_path: 'documents/old_story.txt' - # path: 'documents/story.txt' - # content: 'Lorem ipsum...', - # update: true - # }, - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Rename FILENAME', - # branch: 'master' - # } - # - def rename(repository, options) - commit(repository, options, :rename) - end end def initialize(options) diff --git a/lib/gitlab/git/index.rb b/lib/gitlab/git/index.rb new file mode 100644 index 00000000000..af1744c9c46 --- /dev/null +++ b/lib/gitlab/git/index.rb @@ -0,0 +1,126 @@ +module Gitlab + module Git + class Index + DEFAULT_MODE = 0o100644 + + attr_reader :repository, :raw_index + + def initialize(repository) + @repository = repository + @raw_index = repository.rugged.index + end + + delegate :read_tree, :get, to: :raw_index + + def write_tree + raw_index.write_tree(repository.rugged) + end + + def dir_exists?(path) + raw_index.find { |entry| entry[:path].start_with?("#{path}/") } + end + + def create(options) + options = normalize_options(options) + + file_entry = get(options[:file_path]) + if file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("Filename already exists") + end + + add_blob(options) + end + + def create_dir(options) + options = normalize_options(options) + + file_entry = get(options[:file_path]) + if file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists as a file") + end + + if dir_exists?(options[:file_path]) + raise Gitlab::Git::Repository::InvalidBlobName.new("Directory already exists") + end + + options = options.dup + options[:file_path] += '/.gitkeep' + options[:content] = '' + + add_blob(options) + end + + def update(options) + options = normalize_options(options) + + file_entry = get(options[:file_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + add_blob(options, mode: file_entry[:mode]) + end + + def move(options) + options = normalize_options(options) + + file_entry = get(options[:previous_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + raw_index.remove(options[:previous_path]) + + add_blob(options, mode: file_entry[:mode]) + end + + def delete(options) + options = normalize_options(options) + + file_entry = get(options[:file_path]) + unless file_entry + raise Gitlab::Git::Repository::InvalidBlobName.new("File doesn't exist") + end + + raw_index.remove(options[:file_path]) + end + + private + + def normalize_options(options) + options = options.dup + options[:file_path] = normalize_path(options[:file_path]) if options[:file_path] + options[:previous_path] = normalize_path(options[:previous_path]) if options[:previous_path] + options + end + + def normalize_path(path) + pathname = Gitlab::Git::PathHelper.normalize_path(path.dup) + + if pathname.each_filename.include?('..') + raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') + end + + pathname.to_s + end + + def add_blob(options, mode: nil) + content = options[:content] + content = Base64.decode64(content) if options[:encoding] == 'base64' + + detect = CharlockHolmes::EncodingDetector.new.detect(content) + unless detect && detect[:type] == :binary + # When writing to the repo directly as we are doing here, + # the `core.autocrlf` config isn't taken into account. + content.gsub!("\r\n", "\n") if repository.autocrlf + end + + oid = repository.rugged.write(content, :blob) + + raw_index.add(path: options[:file_path], oid: oid, mode: mode || DEFAULT_MODE) + rescue Rugged::IndexError => e + raise Gitlab::Git::Repository::InvalidBlobName.new(e.message) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 4b6ad8037ce..8ec90885231 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -324,24 +324,30 @@ module Gitlab end def log_by_shell(sha, options) - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log) - cmd += %W(-n #{options[:limit].to_i}) - cmd += %w(--format=%H) - cmd += %W(--skip=#{options[:offset].to_i}) - cmd += %w(--follow) if options[:follow] - cmd += %w(--no-merges) if options[:skip_merges] - cmd += %W(--after=#{options[:after].iso8601}) if options[:after] - cmd += %W(--before=#{options[:before].iso8601}) if options[:before] - cmd += [sha] - cmd += %W(-- #{options[:path]}) if options[:path].present? - - raw_output = IO.popen(cmd) {|io| io.read } - - log = raw_output.lines.map do |c| - Rugged::Commit.new(rugged, c.strip) - end + limit = options[:limit].to_i + offset = options[:offset].to_i + use_follow_flag = options[:follow] && options[:path].present? + + # We will perform the offset in Ruby because --follow doesn't play well with --skip. + # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 + offset_in_ruby = use_follow_flag && options[:offset].present? + limit += offset if offset_in_ruby + + cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log] + cmd << "--max-count=#{limit}" + cmd << '--format=%H' + cmd << "--skip=#{offset}" unless offset_in_ruby + cmd << '--follow' if use_follow_flag + cmd << '--no-merges' if options[:skip_merges] + cmd << "--after=#{options[:after].iso8601}" if options[:after] + cmd << "--before=#{options[:before].iso8601}" if options[:before] + cmd << sha + cmd += %W[-- #{options[:path]}] if options[:path].present? - log.is_a?(Array) ? log : [] + raw_output = IO.popen(cmd) { |io| io.read } + lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines + + lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } end def sha_from_ref(ref) @@ -837,57 +843,6 @@ module Gitlab rugged.config['core.autocrlf'] = AUTOCRLF_VALUES.invert[value] end - # Create a new directory with a .gitkeep file. Creates - # all required nested directories (i.e. mkdir -p behavior) - # - # options should contain next structure: - # author: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # committer: { - # email: 'user@example.com', - # name: 'Test User', - # time: Time.now - # }, - # commit: { - # message: 'Wow such commit', - # branch: 'master', - # update_ref: false - # } - def mkdir(path, options = {}) - # Check if this directory exists; if it does, then don't bother - # adding .gitkeep file. - ref = options[:commit][:branch] - path = Gitlab::Git::PathHelper.normalize_path(path).to_s - rugged_ref = rugged.ref(ref) - - raise InvalidRef.new("Invalid ref") if rugged_ref.nil? - - target_commit = rugged_ref.target - - raise InvalidRef.new("Invalid target commit") if target_commit.nil? - - entry = tree_entry(target_commit, path) - - if entry - if entry[:type] == :blob - raise InvalidBlobName.new("Directory already exists as a file") - else - raise InvalidBlobName.new("Directory already exists") - end - end - - options[:file] = { - content: '', - path: "#{path}/.gitkeep", - update: true - } - - Gitlab::Git::Blob.commit(self, options) - end - # Returns result like "git ls-files" , recursive and full file path # # Ex. diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb index 3fe32adeade..6105d165810 100644 --- a/lib/gitlab/middleware/webpack_proxy.rb +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -8,16 +8,16 @@ module Gitlab @proxy_host = opts.fetch(:proxy_host, 'localhost') @proxy_port = opts.fetch(:proxy_port, 3808) @proxy_path = opts[:proxy_path] if opts[:proxy_path] - super(app, opts) + + super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts) end def perform_request(env) - unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") - return @app.call(env) + if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + super(env) + else + @app.call(env) end - - env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}" - super(env) end end end diff --git a/package.json b/package.json index 66aa7e9fe5d..5528303ab21 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", "underscore": "^1.8.3", - "vue": "^2.0.3", + "vue": "^2.1.10", "vue-resource": "^0.9.3", "webpack": "^2.2.1", "webpack-bundle-analyzer": "^2.3.0" diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index e493b9396f6..46c758b4654 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -125,14 +125,16 @@ describe Projects::IssuesController do end describe 'PUT #update' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it_behaves_like 'update invalid issuable', Issue + context 'when moving issue to another private project' do let(:another_project) { create(:empty_project, :private) } - before do - sign_in(user) - project.team << [user, :developer] - end - context 'when user has access to move issue' do before { another_project.team << [user, :reporter] } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d9cb429132f..1ced666bb36 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -255,6 +255,8 @@ describe Projects::MergeRequestsController do expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } end + + it_behaves_like 'update invalid issuable', MergeRequest end end diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 066a1831852..70e7f9ca96e 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,8 +14,8 @@ describe Projects::TemplatesController do before do project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, 'something valid', - message: 'test 3', branch_name: 'master', update: false) + project.repository.create_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master') end describe '#show' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c80b09e9b9d..586efdefdb3 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -138,27 +138,24 @@ FactoryGirl.define do project.add_user(args[:user], args[:access]) - project.repository.commit_file( + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/bug.md", 'something valid', message: 'test 3', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/template_test.md", 'template_test', message: 'test 1', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( args[:user], ".gitlab/#{args[:path]}/feature_proposal.md", 'feature_proposal', message: 'test 2', - branch_name: 'master', - update: false) + branch_name: 'master') end end end diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb index e8e080ce3e2..273cacd82cd 100644 --- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb +++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb @@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do scenario 'shows only HTTP url' do visit_project - expect(page).to have_content("git clone #{project.http_url_to_repo}") + expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}") expect(page).not_to have_selector('#clone-dropdown') end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index f8c3ccb416b..b740e191f48 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/ + expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/ end end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 1e0db4a0499..1c8267b1593 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include DropzoneHelper include IssueHelpers include SortingHelper include WaitForAjax @@ -570,19 +571,13 @@ describe 'Issues', feature: true do end it 'uploads file when dragging into textarea' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to have_content 'banana_sample' end it 'adds double newline to end of attachment markdown' do - drop_in_dropzone test_image_file - - # Wait for the file to upload - sleep 1 + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') expect(page.find_field("issue_description").value).to match /\n\n$/ end @@ -665,25 +660,4 @@ describe 'Issues', feature: true do end end end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.div-dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end end diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index eb1050d21c6..2f436f153aa 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do scenario 'auto-populates the title', js: true do fill_in('Key', with: attributes_for(:key).fetch(:key)) - expect(find_field('Title').value).to eq 'dummy@gitlab.com' + expect(page).to have_field("Title", with: "dummy@gitlab.com") end scenario 'saves the new key' do diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index 0c51fe72ca4..2352329d58c 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do end def expect_instructions_for(protocol) - msg = :"#{protocol.downcase}_url_to_repo" - - expect(page).to have_content("git clone #{project.send(msg)}") + url = + case protocol + when 'ssh' + project.ssh_url_to_repo + when 'http' + project.http_url_to_repo(developer) + end + + expect(page).to have_content("git clone #{url}") end end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index f8ef4577a26..ccadc936567 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,7 @@ feature 'project owner creates a license file', feature: true, js: true do let(:project_master) { create(:user) } let(:project) { create(:project) } background do - project.repository.remove_file(project_master, 'LICENSE', + project.repository.delete_file(project_master, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index e90a033b8c4..62d0aedda48 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,20 +18,18 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) - project.repository.commit_file( + branch_name: 'master') + project.repository.create_file( user, '.gitlab/issue_templates/test.md', longtemplate_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -79,13 +77,12 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/issue_templates/bug.md', template_content, message: 'added issue template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' fill_in :'issue[description]', with: prior_description @@ -104,13 +101,12 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } background do - project.repository.commit_file( + project.repository.create_file( user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -135,13 +131,12 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - project.repository.commit_file( + project.repository.create_file( fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, message: 'added merge request template', - branch_name: 'master', - update: false) + branch_name: 'master') visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 18ec31f3280..22bf1bfbdf0 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -371,8 +371,14 @@ describe 'Pipelines', :feature, :js do visit new_namespace_project_pipeline_path(project.namespace, project) end - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + context 'for valid commit', js: true do + before do + click_button project.default_branch + + page.within '.dropdown-menu' do + click_link 'master' + end + end context 'with gitlab-ci.yml' do before { stub_ci_pipeline_to_return_yaml_file } @@ -389,15 +395,6 @@ describe 'Pipelines', :feature, :js do it { expect(page).to have_content('Missing .gitlab-ci.yml file') } end end - - context 'for invalid commit' do - before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' - end - - it { expect(page).to have_content('Reference not found') } - end end describe 'Create pipelines' do @@ -409,18 +406,22 @@ describe 'Pipelines', :feature, :js do describe 'new pipeline page' do it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') + expect(page).to have_selector('.js-branch-select') + expect(find('.js-branch-select')).to have_content project.default_branch expect(page).to have_content('Create for') end end describe 'find pipelines' do it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) + click_button project.default_branch - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') + page.within '.dropdown-menu' do + find('.dropdown-input-field').native.send_keys('fix') + + page.within '.dropdown-content' do + expect(page).to have_content('fix') + end end end end diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb new file mode 100644 index 00000000000..f88a515f7fc --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +feature 'User uploads avatar to group', feature: true do + scenario 'they see the new avatar' do + user = create(:user) + group = create(:group) + group.add_owner(user) + login_as(user) + + visit edit_group_path(group) + attach_file( + 'group_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Save group' + + visit group_path(group) + + expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(group.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb new file mode 100644 index 00000000000..0dfd29045e5 --- /dev/null +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +feature 'User uploads avatar to profile', feature: true do + scenario 'they see their new avatar' do + user = create(:user) + login_as(user) + + visit profile_path + attach_file( + 'user_avatar', + Rails.root.join('spec', 'fixtures', 'dk.png'), + visible: false + ) + + click_button 'Update profile settings' + + visit user_path(user) + + expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + + # Cheating here to verify something that isn't user-facing, but is important + expect(user.reload.avatar.file).to exist + end +end diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb new file mode 100644 index 00000000000..0c160dd74b4 --- /dev/null +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +feature 'User uploads file to note', feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user, namespace: user.namespace) } + + scenario 'they see the attached file', js: true do + issue = create(:issue, project: project, author: user) + + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + + dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png')) + click_button 'Comment' + wait_for_ajax + + expect(find('a.no-attachment-icon img[alt="dk"]')['src']) + .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + end +end diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb new file mode 100644 index 00000000000..336c4092c98 --- /dev/null +++ b/spec/features/user_callout_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'User Callouts', js: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } + + before do + login_as(user) + project.team << [user, :master] + end + + it 'takes you to the profile preferences when the link is clicked' do + visit dashboard_projects_path + click_link 'Check it out' + expect(current_path).to eq profile_preferences_path + end + + describe 'user callout should appear in two routes' do + it 'shows up on the user profile' do + visit user_path(user) + expect(find('.user-callout')).to have_content 'Customize your experience' + end + + it 'shows up on the dashboard projects' do + visit dashboard_projects_path + expect(find('.user-callout')).to have_content 'Customize your experience' + end + end + + it 'hides the user callout when click on the dismiss icon' do + visit user_path(user) + within('.user-callout') do + find('.close-user-callout').click + end + expect(page).not_to have_selector('#user-callout') + end +end diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 4397a32fedc..c8a18af7198 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -3,7 +3,9 @@ /* global boardsMockInterceptor */ /* global BoardService */ /* global List */ +/* global ListIssue */ /* global listObj */ +/* global listObjDuplicate */ require('~/lib/utils/url_utility'); require('~/boards/models/issue'); @@ -84,4 +86,23 @@ describe('List model', () => { done(); }, 0); }); + + it('sends service request to update issue label', () => { + const listDup = new List(listObjDuplicate); + const issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [list.label, listDup.label] + }); + + list.issues.push(issue); + listDup.issues.push(issue); + + spyOn(gl.boardService, 'moveIssue').and.callThrough(); + + listDup.updateIssueLabel(list, issue); + + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id); + }); }); diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml new file mode 100644 index 00000000000..275359bde0a --- /dev/null +++ b/spec/javascripts/fixtures/user_callout.html.haml @@ -0,0 +1,2 @@ +.user-callout{ 'callout-svg' => custom_icon('icon_customization') } + diff --git a/spec/javascripts/user_callout_spec.js.es6 b/spec/javascripts/user_callout_spec.js.es6 new file mode 100644 index 00000000000..6ee63f56a26 --- /dev/null +++ b/spec/javascripts/user_callout_spec.js.es6 @@ -0,0 +1,37 @@ +const UserCallout = require('~/user_callout'); + +const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; +const Cookie = window.Cookies; + +describe('UserCallout', () => { + const fixtureName = 'static/user_callout.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(function () { + loadFixtures(fixtureName); + this.userCallout = new UserCallout(); + this.closeButton = $('.close-user-callout'); + this.userCalloutBtn = $('.user-callout-btn'); + this.userCalloutContainer = $('.user-callout'); + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + }); + + afterEach(function () { + Cookie.set(USER_CALLOUT_COOKIE, 'false'); + }); + + it('shows when cookie is set to false', function () { + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBeDefined(); + expect(this.userCalloutContainer.is(':visible')).toBe(true); + }); + + it('hides when user clicks on the dismiss-icon', function () { + this.closeButton.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); + + it('hides when user clicks on the "check it out" button', function () { + this.userCalloutBtn.click(); + expect(Cookie.get(USER_CALLOUT_COOKIE)).toBe('true'); + }); +}); diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb index a2a1ed58d1b..294558b3db2 100644 --- a/spec/lib/banzai/filter/image_link_filter_spec.rb +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -13,8 +13,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do end it 'does not wrap a duplicate link' do - exp = act = %q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>) - expect(filter(act).to_html).to eq exp + doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>)) + expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/ end it 'works with external images' do @@ -22,8 +22,8 @@ describe Banzai::Filter::ImageLinkFilter, lib: true do expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] end - it 'wraps the image with a link and a div' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.to_html).to include('<div class="image-container">') + it 'works with inline images' do + doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>)) + expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/ end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 0c321f0343c..8049e2c120d 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -222,191 +222,6 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe :commit do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - content: 'Lorem ipsum...', - path: 'documents/story.txt' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Wow such commit', - branch: 'fix-mode' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - - it 'should add file with commit' do - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' } - - # Directory was created - expect(tree[:type]).to eq(:tree) - - # File was created - expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt') - end - - describe "ref updating" do - it 'creates a commit but does not udate a ref' do - commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false} - commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts) - commit = repository.lookup(commit_sha) - - # Commit message valid - expect(commit.message).to eq('Wow such commit') - - # Does not update any related ref - expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid) - expect(repository.lookup("HEAD").oid).not_to eq(commit.oid) - end - end - - describe 'reject updates' do - it 'should reject updates' do - commit_options[:file][:update] = false - commit_options[:file][:path] = 'files/executables/ls' - - expect{ commit_sha }.to raise_error('Filename already exists; update not allowed') - end - end - - describe 'file modes' do - it 'should preserve file modes with commit' do - commit_options[:file][:path] = 'files/executables/ls' - - entry = Gitlab::Git::Blob.find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path]) - expect(entry[:filemode]).to eq(0100755) - end - end - end - - describe :rename do - let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) } - let(:rename_options) do - { - file: { - path: 'NEWCONTRIBUTING.md', - previous_path: 'CONTRIBUTING.md', - content: 'Lorem ipsum...', - update: true - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Rename readme', - branch: 'master' - } - } - end - - let(:rename_options2) do - { - file: { - content: 'Lorem ipsum...', - path: 'bin/new_executable', - previous_path: 'bin/executable', - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Updates toberenamed.txt', - branch: 'master', - update_ref: false - } - } - end - - it 'maintains file permissions when renaming' do - mode = 0o100755 - - Gitlab::Git::Blob.rename(repository, rename_options2) - - expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode) - end - - it 'renames the file with commit and not change file permissions' do - ref = rename_options[:commit][:branch] - - expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil - expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1) - - expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil - expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil - end - end - - describe :remove do - let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } - - let(:commit_options) do - { - file: { - path: 'README.md' - }, - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Remove readme', - branch: 'feature' - } - } - end - - let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) } - let(:commit) { repository.lookup(commit_sha) } - let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") } - - it 'should remove file with commit' do - # Commit message valid - expect(commit.message).to eq('Remove readme') - - # File was removed - expect(blob).to be_nil - end - end - describe :lfs_pointers do context 'file a valid lfs pointer' do let(:blob) do diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb new file mode 100644 index 00000000000..d0c7ca60ddc --- /dev/null +++ b/spec/lib/gitlab/git/index_spec.rb @@ -0,0 +1,220 @@ +require 'spec_helper' + +describe Gitlab::Git::Index, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } + let(:index) { described_class.new(repository) } + + before do + index.read_tree(repository.lookup('master').tree) + end + + describe '#create' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'documents/story.txt' + } + end + + context 'when no file at that path exists' do + it 'creates the file in the index' do + index.create(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create(options) }.to raise_error('Filename already exists') + end + end + + context 'when content is in base64' do + before do + options[:content] = Base64.encode64(options[:content]) + options[:encoding] = 'base64' + end + + it 'decodes base64' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) + end + end + + context 'when content contains CRLF' do + before do + repository.autocrlf = :input + options[:content] = "Hello,\r\nWorld" + end + + it 'converts to LF' do + index.create(options) + + entry = index.get(options[:file_path]) + expect(repository.lookup(entry[:oid]).content).to eq("Hello,\nWorld") + end + end + end + + describe '#create_dir' do + let(:options) do + { + file_path: 'newdir' + } + end + + context 'when no file or dir at that path exists' do + it 'creates the dir in the index' do + index.create_dir(options) + + entry = index.get(options[:file_path] + '/.gitkeep') + + expect(entry).not_to be_nil + end + end + + context 'when a file at that path exists' do + before do + options[:file_path] = 'files/executables/ls' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists as a file') + end + end + + context 'when a directory at that path exists' do + before do + options[:file_path] = 'files/executables' + end + + it 'raises an error' do + expect { index.create_dir(options) }.to raise_error('Directory already exists') + end + end + end + + describe '#update' do + let(:options) do + { + content: 'Lorem ipsum...', + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.update(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'updates the file in the index' do + index.update(options) + + entry = index.get(options[:file_path]) + + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:file_path] = 'files/executables/ls' + + index.update(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#move' do + let(:options) do + { + content: 'Lorem ipsum...', + previous_path: 'README.md', + file_path: 'NEWREADME.md' + } + end + + context 'when no file at that path exists' do + it 'raises an error' do + options[:previous_path] = 'documents/story.txt' + + expect { index.move(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the old file in the index' do + index.move(options) + + entry = index.get(options[:previous_path]) + + expect(entry).to be_nil + end + + it 'creates the new file in the index' do + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry).not_to be_nil + expect(repository.lookup(entry[:oid]).content).to eq(options[:content]) + end + + it 'preserves file mode' do + options[:previous_path] = 'files/executables/ls' + + index.move(options) + + entry = index.get(options[:file_path]) + + expect(entry[:mode]).to eq(0100755) + end + end + end + + describe '#delete' do + let(:options) do + { + file_path: 'README.md' + } + end + + context 'when no file at that path exists' do + before do + options[:file_path] = 'documents/story.txt' + end + + it 'raises an error' do + expect { index.delete(options) }.to raise_error("File doesn't exist") + end + end + + context 'when a file at that path exists' do + it 'removes the file in the index' do + index.delete(options) + + entry = index.get(options[:file_path]) + + expect(entry).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 2a915bf426f..1919542ca25 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do commit_with_new_name = nil rename_commit = nil - before(:all) do + before(:context) do # Add new commits so that there's a renamed file in the commit history repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged @@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do commit_with_new_name = new_commit_edit_new_file(repo) end + after(:context) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) + end + context "where 'follow' == true" do - options = { ref: "master", follow: true } + let(:options) { { ref: "master", follow: true } } context "and 'path' is a directory" do - let(:log_commits) do - repository.log(options.merge(path: "encoding")) - end + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "encoding")) - it "should not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end end context "and 'path' is a file that matches the new filename" do - let(:log_commits) do - repository.log(options.merge(path: "encoding/CHANGELOG")) + context 'without offset' do + it "follows renames" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) + + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end - it "should follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + context 'with offset=1' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + end + + context 'with offset=1', 'and limit=1' do + it "follows renames, skip the latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) + + expect(log_commits).to contain_exactly(rename_commit) + end + end + + context 'with offset=1', 'and limit=2' do + it "follows renames, skip the latest commit and return only two commits" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) + + aggregate_failures do + expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + end + end + end + + context 'with offset=2' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + end + + context 'with offset=2', 'and limit=1' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) + + expect(log_commits).to contain_exactly(commit_with_old_name) + end + end + + context 'with offset=2', 'and limit=2' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end end context "and 'path' is a file that matches the old filename" do - let(:log_commits) do - repository.log(options.merge(path: "CHANGELOG")) - end + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "CHANGELOG")) - it "should not follow renames" do - expect(log_commits).to include(commit_with_old_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_new_name) + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end end end context "unknown ref" do - let(:log_commits) { repository.log(options.merge(ref: 'unknown')) } + it "returns an empty array" do + log_commits = repository.log(options.merge(ref: 'unknown')) - it "should return empty" do expect(log_commits).to eq([]) end end @@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end - - after(:all) do - # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged - repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) - end end describe "#commits_between" do @@ -844,81 +908,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#mkdir' do - let(:commit_options) do - { - author: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - committer: { - email: 'user@example.com', - name: 'Test User', - time: Time.now - }, - commit: { - message: 'Test message', - branch: 'refs/heads/fix', - } - } - end - - def generate_diff_for_path(path) - "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep -new file mode 100644 -index 0000000..e69de29 ---- /dev/null -+++ b/#{path}/.gitkeep\n" - end - - shared_examples 'mkdir diff check' do |path, expected_path| - it 'creates a directory' do - result = repository.mkdir(path, commit_options) - expect(result).not_to eq(nil) - - # Verify another mkdir doesn't create a directory that already exists - expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists') - end - end - - describe 'creates a directory in root directory' do - it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir' - end - - describe 'creates a directory in subdirectory' do - it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test' - end - - describe 'creates a directory in subdirectory with a slash' do - it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2' - end - - describe 'creates a directory in subdirectory with multiple slashes' do - it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3' - end - - describe 'handles relative paths' do - it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative' - end - - describe 'creates nested directories' do - it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test' - end - - it 'does not attempt to create a directory with invalid relative path' do - expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path') - end - - it 'does not attempt to overwrite a file' do - expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file') - end - - it 'does not attempt to overwrite a directory' do - expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists') - end - end - describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index d37890de9ae..48f7754bed8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -209,13 +209,12 @@ describe Gitlab::GitAccess, lib: true do stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') target_branch = project.repository.lookup('feature') - source_branch = project.repository.commit_file( + source_branch = project.repository.create_file( user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, message: FFaker::HipsterIpsum.sentence, - branch_name: unprotected_branch, - update: false) + branch_name: unprotected_branch) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index b9fe492fe2c..e6a826a9418 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,13 +21,12 @@ describe 'CycleAnalytics#production', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 78ec518ac86..3a02ed81adb 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -26,13 +26,12 @@ describe 'CycleAnalytics#staging', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file( + sha = context.project.repository.create_file( context.user, context.random_git_name, 'content', message: 'commit message', - branch_name: 'master', - update: false) + branch_name: 'master') context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fa1b0396bcf..9331dc41a5e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -833,12 +833,6 @@ describe MergeRequest, models: true do it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') end - - it 'creates Todo on unmergeability' do - expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject) - - subject.check_if_can_be_merged - end end context 'when it has conflicts' do diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 59a4ae1b799..9b711bfc007 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -7,12 +7,27 @@ describe ProjectGroupLink do end describe "Validation" do - let!(:project_group_link) { create(:project_group_link) } + let(:parent_group) { create(:group) } + let(:group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: group) } + let!(:project_group_link) { create(:project_group_link, project: project) } it { should validate_presence_of(:project_id) } it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } it { should validate_presence_of(:group) } it { should validate_presence_of(:group_access) } + + it "doesn't allow a project to be shared with the group it is in" do + project_group_link.group = group + + expect(project_group_link).not_to be_valid + end + + it "doesn't allow a project to be shared with an ancestor of the group it is in" do + project_group_link.group = parent_group + + expect(project_group_link).not_to be_valid + end end describe "destroying a record", truncate: true do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8a27dfff203..ee4f4092062 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1765,7 +1765,7 @@ describe Project, models: true do end before do - project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + project.repository.create_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do @@ -1894,4 +1894,25 @@ describe Project, models: true do end end end + + describe '#http_url_to_repo' do + let(:project) { create :empty_project } + + context 'when no user is given' do + it 'returns the url to the repo without a username' do + url = project.http_url_to_repo + + expect(url).to eq(project.http_url_to_repo) + expect(url).not_to include('@') + end + end + + context 'when user is given' do + it 'returns the url to the repo with the username' do + user = build_stubbed(:user) + + expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@}) + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 43aac31d8b7..ae203fada12 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -291,10 +291,10 @@ describe Repository, models: true do end end - describe "#commit_dir" do + describe "#create_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -307,7 +307,7 @@ describe Repository, models: true do it "creates a fork and commit to the forked project" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', start_branch_name: 'master', start_project: forked_project) end.to change { repository.commits('master').count }.by(0) @@ -323,7 +323,7 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_dir(user, 'newdir', + repository.create_dir(user, 'newdir', message: 'Add newdir', branch_name: 'master', author_email: author_email, author_name: author_name) @@ -337,25 +337,23 @@ describe Repository, models: true do end end - describe "#commit_file" do - it 'commits change to a file successfully' do + describe "#create_file" do + it 'commits new file successfully' do expect do - repository.commit_file(user, 'CHANGELOG', 'Changelog!', - message: 'Updates file content', - branch_name: 'master', - update: true) + repository.create_file(user, 'NEWCHANGELOG', 'Changelog!', + message: 'Create changelog', + branch_name: 'master') end.to change { repository.commits('master').count }.by(1) - blob = repository.blob_at('master', 'CHANGELOG') + blob = repository.blob_at('master', 'NEWCHANGELOG') expect(blob.data).to eq('Changelog!') end it 'respects the autocrlf setting' do - repository.commit_file(user, 'hello.txt', "Hello,\r\nWorld", + repository.create_file(user, 'hello.txt', "Hello,\r\nWorld", message: 'Add hello world', - branch_name: 'master', - update: true) + branch_name: 'master') blob = repository.blob_at('master', 'hello.txt') @@ -365,10 +363,9 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_file(user, 'README', 'README!', + repository.create_file(user, 'NEWREADME', 'README!', message: 'Add README', branch_name: 'master', - update: true, author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -382,6 +379,18 @@ describe Repository, models: true do end describe "#update_file" do + it 'updates file successfully' do + expect do + repository.update_file(user, 'CHANGELOG', 'Changelog!', + message: 'Update changelog', + branch_name: 'master') + end.to change { repository.commits('master').count }.by(1) + + blob = repository.blob_at('master', 'CHANGELOG') + + expect(blob.data).to eq('Changelog!') + end + it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', @@ -398,9 +407,6 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do repository.update_file(user, 'README', 'Updated README!', branch_name: 'master', @@ -418,13 +424,10 @@ describe Repository, models: true do end end - describe "#remove_file" do + describe "#delete_file" do it 'removes file successfully' do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) @@ -433,11 +436,8 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, 'README', 'README!', - message: 'Add README', branch_name: 'master', update: true) - expect do - repository.remove_file(user, 'README', + repository.delete_file(user, 'README', message: 'Remove README', branch_name: 'master', author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) @@ -587,14 +587,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file( + repository.delete_file( user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file( + repository.create_file( user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -603,27 +603,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'markdown') - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'markdown', update: false) + message: 'Add LICENSE', branch_name: 'markdown') expect(repository.license_blob).to be_nil end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_blob.name).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| it "detects '#{filename}'" do - repository.commit_file(user, filename, + repository.create_file(user, filename, Licensee::License.new('mit').content, - message: "Add #{filename}", branch_name: 'master', update: false) + message: "Add #{filename}", branch_name: 'master') expect(repository.license_blob.name).to eq(filename) end @@ -632,7 +632,7 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', + repository.delete_file(user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end @@ -647,16 +647,16 @@ describe Repository, models: true do end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', - message: 'Add LICENSE', branch_name: 'master', update: false) + repository.create_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', + repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, - message: 'Add LICENSE', branch_name: 'master', update: false) + message: 'Add LICENSE', branch_name: 'master') expect(repository.license_key).to eq('mit') end @@ -913,10 +913,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) - empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + empty_repository.create_file(user, 'CHANGELOG', 'Changelog!', message: 'Updates file content', - branch_name: 'master', - update: false) + branch_name: 'master') end end end @@ -1796,7 +1795,7 @@ describe Repository, models: true do describe '#gitlab_ci_yml_for' do before do - repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master') end context 'when there is a .gitlab-ci.yml at the commit' do @@ -1814,7 +1813,7 @@ describe Repository, models: true do describe '#route_map_for' do before do - repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master') end context 'when there is a .gitlab/route-map.yml at the commit' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6356f8b6c92..e86b4a761d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1429,7 +1429,7 @@ describe User, models: true do it { expect(user.nested_groups).to eq([nested_group]) } end - describe '#nested_projects' do + describe '#nested_groups_projects' do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } @@ -1444,7 +1444,7 @@ describe User, models: true do other_project.add_developer(create(:user)) end - it { expect(user.nested_projects).to eq([nested_project]) } + it { expect(user.nested_groups_projects).to eq([nested_project]) } end describe '#refresh_authorized_projects', redis: true do diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 919c98d6437..46edbd49b28 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -200,7 +200,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end end @@ -210,7 +210,7 @@ describe API::AccessRequests, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.requesters.count }.by(-1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 6cc1ef315db..9756991162e 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -242,9 +242,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) - end.to change { issue.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { issue.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when the award emoji can not be found' do @@ -258,9 +258,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) - end.to change { merge_request.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { merge_request.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when note id not found' do @@ -277,9 +277,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) - end.to change { snippet.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { snippet.award_emoji.count }.from(1).to(0) end end end @@ -290,9 +290,9 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) - end.to change { note.award_emoji.count }.from(1).to(0) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change { note.award_emoji.count }.from(1).to(0) end end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index 71df534ebe1..87c36639cd4 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -195,8 +195,7 @@ describe API::Boards, api: true do it "deletes the list if an admin requests it" do delete api("#{base_url}/#{dev_list.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['position']).to eq(1) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index cacdb21c692..ab5a7e4d3de 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -325,15 +325,14 @@ describe API::Branches, api: true do it "removes branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - expect(response).to have_http_status(200) - expect(json_response['branch']).to eq(branch_name) + + expect(response).to have_http_status(204) end it "removes a branch with dots in the branch name" do delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user) - expect(response).to have_http_status(200) - expect(json_response['branch']).to eq("with.1.2.3") + expect(response).to have_http_status(204) end it 'returns 404 if branch not exists' do diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 921d8714173..024fa66848c 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -174,8 +174,11 @@ describe API::BroadcastMessages, api: true do end it 'deletes the broadcast message for admins' do - expect { delete api("/broadcast_messages/#{message.id}", admin) } - .to change { BroadcastMessage.count }.by(-1) + expect do + delete api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(204) + end.to change { BroadcastMessage.count }.by(-1) end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 7e682e91bd1..4f4b18cf0e0 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -116,6 +116,8 @@ describe API::DeployKeys, api: true do it 'should delete existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + + expect(response).to have_http_status(204) end.to change{ project.deploy_keys.count }.by(-1) end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index d0958d39d44..d66eb63fd0a 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -122,7 +122,7 @@ describe API::Environments, api: true do it 'returns a 200 for an existing environment' do delete api("/projects/#{project.id}/environments/#{environment.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns a 404 for non existing id' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index a8ce0430401..31b1aca6d73 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -127,7 +127,7 @@ describe API::Files, api: true do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) post api("/projects/#{project.id}/repository/files", user), valid_params @@ -201,11 +201,7 @@ describe API::Files, api: true do it "deletes existing file in project repo" do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(user.email) - expect(last_commit.author_name).to eq(user.name) + expect(response).to have_http_status(204) end it "returns a 400 bad request if no params given" do @@ -215,7 +211,7 @@ describe API::Files, api: true do end it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) delete api("/projects/#{project.id}/repository/files", user), valid_params @@ -228,10 +224,7 @@ describe API::Files, api: true do delete api("/projects/#{project.id}/repository/files", user), valid_params - expect(response).to have_http_status(200) - last_commit = project.repository.commit.raw - expect(last_commit.author_email).to eq(author_email) - expect(last_commit.author_name).to eq(author_name) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index fb3dc1b074e..b0ba3ea912d 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -467,7 +467,7 @@ describe API::Groups, api: true do it "removes group" do delete api("/groups/#{group1.id}", user1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a group if not an owner" do @@ -496,7 +496,7 @@ describe API::Groups, api: true do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it "does not remove a non existing group" do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 7cb75310204..ddc2e51821e 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1175,8 +1175,8 @@ describe API::Issues, api: true do it "deletes the issue if an admin requests it" do delete api("/projects/#{project.id}/issues/#{issue.id}", owner) - expect(response).to have_http_status(200) - expect(json_response['state']).to eq 'opened' + + expect(response).to have_http_status(204) end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index af271dbd4f5..a1adaba7b98 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -175,9 +175,10 @@ describe API::Labels, api: true do end describe 'DELETE /projects/:id/labels' do - it 'returns 200 for existing label' do + it 'returns 204 for existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label1' - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) end it 'returns 404 for non existing label' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 31166b50033..2d37d026a39 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -173,11 +173,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access_level is not valid' do + it 'returns 400 when access_level is not valid' do post api("/#{source_type.pluralize}/#{source.id}/members", master), user_id: stranger.id, access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -230,11 +230,11 @@ describe API::Members, api: true do expect(response).to have_http_status(400) end - it 'returns 422 when access level is not valid' do + it 'returns 400 when access level is not valid' do put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), access_level: 1234 - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end end end @@ -263,18 +263,18 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.members.count }.by(-1) end end context 'when authenticated as a master/owner' do context 'and member is a requester' do - it "returns #{source_type == 'project' ? 200 : 404}" do + it 'returns 404' do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end.not_to change { source.requesters.count } end end @@ -283,15 +283,15 @@ describe API::Members, api: true do expect do delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end.to change { source.members.count }.by(-1) end end - it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do + it 'returns 404 if member does not exist' do delete api("/#{source_type.pluralize}/#{source.id}/members/123", master) - expect(response).to have_http_status(source_type == 'project' ? 200 : 404) + expect(response).to have_http_status(404) end end end @@ -342,7 +342,7 @@ describe API::Members, api: true do post api("/projects/#{project.id}/members", master), user_id: stranger.id, access_level: Member::OWNER - expect(response).to have_http_status(422) + expect(response).to have_http_status(400) end.to change { project.members.count }.by(0) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b87d0cd7de9..5522154899c 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -411,7 +411,7 @@ describe API::MergeRequests, api: true do it "destroys the merge request owners can destroy" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 3cca4468be7..9d3c821b692 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -373,7 +373,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) @@ -392,7 +392,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user) @@ -412,7 +412,7 @@ describe API::Notes, api: true do delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/merge_requests/"\ "#{merge_request.id}/notes/#{merge_request_note.id}", user) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 20c76bd2c05..f286568547d 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -183,13 +183,9 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do it "deletes hook from project" do expect do delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - end.to change {project.hooks.count}.by(-1) - expect(response).to have_http_status(200) - end - it "returns success when deleting hook" do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) + end.to change {project.hooks.count}.by(-1) end it "returns a 404 error when deleting non existent hook" do diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index da9df56401b..2c4602faf2c 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -189,7 +189,7 @@ describe API::ProjectSnippets, api: true do delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) end it 'returns 404 for invalid snippet id' do @@ -212,7 +212,7 @@ describe API::ProjectSnippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/projects/#{snippet.project.id}/snippets/1234", admin) + get api("/projects/#{snippet.project.id}/snippets/1234/raw", admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5de4426f3bd..3a00d974633 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -269,10 +269,37 @@ describe API::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post api('/projects', user), name: 'foo' }. + it 'creates new project without path but with name and returns 201' do + expect { post api('/projects', user), name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-project') + end + + it 'creates new project without name but with path and returns 201' do + expect { post api('/projects', user), path: 'foo_project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('foo_project') + expect(project.path).to eq('foo_project') + end + + it 'creates new project name and path and returns 201' do + expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. to change { Project.count }.by(1) expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-Project') end it 'creates last project before reaching project limit' do @@ -281,7 +308,7 @@ describe API::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + it 'does not create new project without name or path and returns 400' do expect { post api('/projects', user) }.not_to change { Project.count } expect(response).to have_http_status(400) end @@ -820,8 +847,9 @@ describe API::Projects, api: true do it 'deletes existing project snippet' do expect do delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(204) end.to change { Snippet.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 when deleting unknown snippet id' do @@ -905,8 +933,10 @@ describe API::Projects, api: true do project_fork_target.reload expect(project_fork_target.forked_from_project).not_to be_nil expect(project_fork_target.forked?).to be_truthy + delete api("/projects/#{project_fork_target.id}/fork", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) project_fork_target.reload expect(project_fork_target.forked_from_project).to be_nil expect(project_fork_target.forked?).not_to be_truthy diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb new file mode 100644 index 00000000000..e83202e4196 --- /dev/null +++ b/spec/requests/api/runner_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +describe API::Runner do + include ApiHelpers + include StubGitlabCalls + + let(:registration_token) { 'abcdefg123456' } + + before do + stub_gitlab_calls + stub_application_setting(runners_registration_token: registration_token) + end + + describe '/api/v4/runners' do + describe 'POST /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + post api('/runners') + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + it 'creates runner with default values' do + post api('/runners'), token: registration_token + + runner = Ci::Runner.first + + expect(response).to have_http_status 201 + expect(json_response['id']).to eq(runner.id) + expect(json_response['token']).to eq(runner.token) + expect(runner.run_untagged).to be true + end + + context 'when project token is used' do + let(:project) { create(:empty_project) } + + it 'creates runner' do + post api('/runners'), token: project.runners_token + + expect(response).to have_http_status 201 + expect(project.runners.size).to eq(1) + end + end + end + + context 'when runner description is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + description: 'server.hostname' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.description).to eq('server.hostname') + end + end + + context 'when runner tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + tag_list: 'tag1, tag2' + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + end + end + + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be false + expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) + end + end + + context 'when tags are not provided' do + it 'returns 404 error' do + post api('/runners'), token: registration_token, + run_untagged: false + + expect(response).to have_http_status 404 + end + end + end + + context 'when option for locking Runner is provided' do + it 'creates runner' do + post api('/runners'), token: registration_token, + locked: true + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.locked).to be true + end + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' info is present" do + let(:value) { "#{param}_value" } + + it %q(updates provided Runner's parameter) do + post api('/runners'), token: registration_token, + info: { param => value } + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + end + end + end + end + + describe 'DELETE /api/v4/runners' do + context 'when no token is provided' do + it 'returns 400 error' do + delete api('/runners') + + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + delete api('/runners'), token: 'invalid' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + let(:runner) { create(:ci_runner) } + + it 'deletes Runner' do + delete api('/runners'), token: runner.token + + expect(response).to have_http_status 204 + expect(Ci::Runner.count).to eq(0) + end + end + end + end +end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 103d6755888..8a82543a830 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -277,8 +277,9 @@ describe API::Runners, api: true do it 'deletes runner' do expect do delete api("/runners/#{shared_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.shared.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -286,15 +287,17 @@ describe API::Runners, api: true do it 'deletes unused runner' do expect do delete api("/runners/#{unused_specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end it 'deletes used runner' do expect do delete api("/runners/#{specific_runner.id}", admin) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end @@ -327,8 +330,9 @@ describe API::Runners, api: true do it 'deletes runner for one owned project' do expect do delete api("/runners/#{specific_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ Ci::Runner.specific.count }.by(-1) - expect(response).to have_http_status(200) end end end @@ -457,8 +461,9 @@ describe API::Runners, api: true do it "disables project's runner" do expect do delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + + expect(response).to have_http_status(204) end.to change{ project.runners.count }.by(-1) - expect(response).to have_http_status(200) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 776dc655650..fd334934ca5 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -55,7 +55,7 @@ describe API::Services, api: true do it "deletes #{service}" do delete api("/projects/#{project.id}/services/#{dashed_service}", user) - expect(response).to have_http_status(200) + expect(response).to have_http_status(204) project.send(service_method).reload expect(project.send(service_method).activated?).to be_falsey end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 41def7cd1d4..5219f6eed42 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -74,7 +74,7 @@ describe API::Snippets, api: true do end it 'returns 404 for invalid snippet id' do - delete api("/snippets/1234", user) + get api("/snippets/1234/raw", user) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 Snippet Not Found') diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b59da632c00..d1e10f12657 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -91,6 +91,8 @@ describe API::SystemHooks, api: true do it "deletes a hook" do expect do delete api("/hooks/#{hook.id}", admin) + + expect(response).to have_http_status(204) end.to change { SystemHook.count }.by(-1) end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 8a4f078182f..b132d033a61 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -137,8 +137,8 @@ describe API::Tags, api: true do context 'delete tag' do it 'deletes an existing tag' do delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user) - expect(response).to have_http_status(200) - expect(json_response['tag_name']).to eq(tag_name) + + expect(response).to have_http_status(204) end it 'raises 404 if the tag does not exist' do diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 92dfc2aa277..153e2791cbe 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -190,8 +190,9 @@ describe API::Triggers do it 'deletes trigger' do expect do delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(204) end.to change{project.triggers.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing trigger' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 603da9f49fc..e5e4c84755f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -540,10 +540,12 @@ describe API::Users, api: true do it 'deletes existing key' do user.keys << key user.save + expect do delete api("/users/#{user.id}/keys/#{key.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.keys.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -637,10 +639,12 @@ describe API::Users, api: true do it 'deletes existing email' do user.emails << email user.save + expect do delete api("/users/#{user.id}/emails/#{email.id}", admin) + + expect(response).to have_http_status(204) end.to change { user.emails.count }.by(-1) - expect(response).to have_http_status(200) end it 'returns 404 error if user not found' do @@ -671,10 +675,10 @@ describe API::Users, api: true do it "deletes user" do delete api("/users/#{user.id}", admin) - expect(response).to have_http_status(200) + + expect(response).to have_http_status(204) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound - expect(json_response['email']).to eq(user.email) end it "does not delete for unauthenticated user" do @@ -869,10 +873,12 @@ describe API::Users, api: true do it "deletes existed key" do user.keys << key user.save + expect do delete api("/user/keys/#{key.id}", user) + + expect(response).to have_http_status(204) end.to change{user.keys.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if key ID not found" do @@ -976,10 +982,12 @@ describe API::Users, api: true do it "deletes existed email" do user.emails << email user.save + expect do delete api("/user/emails/#{email.id}", user) + + expect(response).to have_http_status(204) end.to change{user.emails.count}.by(-1) - expect(response).to have_http_status(200) end it "returns 404 if email ID not found" do diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb new file mode 100644 index 00000000000..91145c8e72c --- /dev/null +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe API::V3::AwardEmoji, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } + let!(:note) { create(:note, project: project, noteable: issue) } + + before { project.team << [user, :master] } + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do + context 'when the awardable is an Issue' do + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + + expect(response).to have_http_status(200) + end.to change { issue.award_emoji.count }.from(1).to(0) + end + + it 'returns a 404 error when the award emoji can not be found' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when the awardable is a Merge Request' do + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + + expect(response).to have_http_status(200) + end.to change { merge_request.award_emoji.count }.from(1).to(0) + end + + it 'returns a 404 error when note id not found' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'when the awardable is a Snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet, user: user) } + + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + end.to change { snippet.award_emoji.count }.from(1).to(0) + end + end + end + + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } + + it 'deletes the award' do + expect do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + + expect(response).to have_http_status(200) + end.to change { note.award_emoji.count }.from(1).to(0) + end + end +end diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb index 8aaf3be4f87..eb95934f354 100644 --- a/spec/requests/api/v3/boards_spec.rb +++ b/spec/requests/api/v3/boards_spec.rb @@ -5,6 +5,7 @@ describe API::V3::Boards, api: true do let(:user) { create(:user) } let(:guest) { create(:user) } + let(:non_member) { create(:user) } let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } let!(:dev_label) do @@ -76,4 +77,37 @@ describe API::V3::Boards, api: true do expect(response).to have_http_status(404) end end + + describe "DELETE /projects/:id/board/lists/:list_id" do + let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete v3_api("#{base_url}/#{dev_list.id}", non_member) + + expect(response).to have_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete v3_api("#{base_url}/#{dev_list.id}", guest) + + expect(response).to have_http_status(403) + end + + it "returns 404 error if list id not found" do + delete v3_api("#{base_url}/44444", user) + + expect(response).to have_http_status(404) + end + + context "when the user is project owner" do + let(:owner) { create(:user) } + let(:project) { create(:empty_project, namespace: owner.namespace) } + + it "deletes the list if an admin requests it" do + delete v3_api("#{base_url}/#{dev_list.id}", owner) + + expect(response).to have_http_status(200) + end + end + end end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb index a3e1581fcc5..e4cedf98e64 100644 --- a/spec/requests/api/v3/branches_spec.rb +++ b/spec/requests/api/v3/branches_spec.rb @@ -5,8 +5,12 @@ describe API::V3::Branches, api: true do include ApiHelpers let(:user) { create(:user) } + let(:user2) { create(:user) } let!(:project) { create(:project, :repository, creator: user) } let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } + let!(:branch_name) { 'feature' } + let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") } describe "GET /projects/:id/repository/branches" do it "returns an array of project branches" do @@ -21,6 +25,44 @@ describe API::V3::Branches, api: true do end end + describe "DELETE /projects/:id/repository/branches/:branch" do + before do + allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) + end + + it "removes branch" do + delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq(branch_name) + end + + it "removes a branch with dots in the branch name" do + delete v3_api("/projects/#{project.id}/repository/branches/with.1.2.3", user) + + expect(response).to have_http_status(200) + expect(json_response['branch_name']).to eq("with.1.2.3") + end + + it 'returns 404 if branch not exists' do + delete v3_api("/projects/#{project.id}/repository/branches/foobar", user) + expect(response).to have_http_status(404) + end + + it "removes protected branch" do + create(:protected_branch, project: project, name: branch_name) + delete v3_api("/projects/#{project.id}/repository/branches/#{branch_name}", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Protected branch cant be removed') + end + + it "does not remove HEAD branch" do + delete v3_api("/projects/#{project.id}/repository/branches/master", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('Cannot remove HEAD branch') + end + end + describe "DELETE /projects/:id/repository/merged_branches" do before do allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true) @@ -33,10 +75,7 @@ describe API::V3::Branches, api: true do end it 'returns a 403 error if guest' do - user_b = create :user - create(:project_member, :guest, user: user_b, project: project) - - delete v3_api("/projects/#{project.id}/repository/merged_branches", user_b) + delete v3_api("/projects/#{project.id}/repository/merged_branches", user2) expect(response).to have_http_status(403) end diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb new file mode 100644 index 00000000000..06556401a29 --- /dev/null +++ b/spec/requests/api/v3/broadcast_messages_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe API::V3::BroadcastMessages, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'DELETE /broadcast_messages/:id' do + let!(:message) { create(:broadcast_message) } + + it 'returns a 401 for anonymous users' do + delete v3_api("/broadcast_messages/#{message.id}"), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + delete v3_api("/broadcast_messages/#{message.id}", user), + attributes_for(:broadcast_message) + + expect(response).to have_http_status(403) + end + + it 'deletes the broadcast message for admins' do + expect do + delete v3_api("/broadcast_messages/#{message.id}", admin) + + expect(response).to have_http_status(200) + end.to change { BroadcastMessage.count }.by(-1) + end + end +end diff --git a/spec/requests/api/v3/environments_spec.rb b/spec/requests/api/v3/environments_spec.rb new file mode 100644 index 00000000000..1ac666ab240 --- /dev/null +++ b/spec/requests/api/v3/environments_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe API::V3::Environments, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + end + + describe 'DELETE /projects/:id/environments/:environment_id' do + context 'as a master' do + it 'returns a 200 for an existing environment' do + delete v3_api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 404 for non existing id' do + delete v3_api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + delete v3_api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 4af05605ec6..93637053626 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -2,17 +2,6 @@ require 'spec_helper' describe API::V3::Files, api: true do include ApiHelpers - let(:user) { create(:user) } - let!(:project) { create(:project, :repository, namespace: user.namespace ) } - let(:guest) { create(:user) { |u| project.add_guest(u) } } - let(:file_path) { 'files/ruby/popen.rb' } - let(:params) do - { - file_path: file_path, - ref: 'master' - } - end - let(:author_email) { FFaker::Internet.email } # I have to remove periods from the end of the name # This happened when the user's name had a suffix (i.e. "Sr.") @@ -26,6 +15,18 @@ describe API::V3::Files, api: true do # ... # Author: Foo Sr <foo@example.com> # ... + + let(:user) { create(:user) } + let!(:project) { create(:project, :repository, namespace: user.namespace ) } + let(:guest) { create(:user) { |u| project.add_guest(u) } } + let(:file_path) { 'files/ruby/popen.rb' } + let(:params) do + { + file_path: file_path, + ref: 'master' + } + end + let(:author_email) { FFaker::Internet.email } let(:author_name) { FFaker::Name.name.chomp("\.") } before { project.team << [user, :developer] } @@ -127,7 +128,7 @@ describe API::V3::Files, api: true do end it "returns a 400 if editor fails to create file" do - allow_any_instance_of(Repository).to receive(:commit_file). + allow_any_instance_of(Repository).to receive(:create_file). and_return(false) post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -215,7 +216,7 @@ describe API::V3::Files, api: true do end it "returns a 400 if fails to create file" do - allow_any_instance_of(Repository).to receive(:remove_file).and_return(false) + allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) delete v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb index f44403374e9..dfac357d37c 100644 --- a/spec/requests/api/v3/labels_spec.rb +++ b/spec/requests/api/v3/labels_spec.rb @@ -149,4 +149,23 @@ describe API::V3::Labels, api: true do end end end + + describe 'DELETE /projects/:id/labels' do + it 'returns 200 for existing label' do + delete v3_api("/projects/#{project.id}/labels", user), name: 'label1' + + expect(response).to have_http_status(200) + end + + it 'returns 404 for non existing label' do + delete v3_api("/projects/#{project.id}/labels", user), name: 'label2' + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Label Not Found') + end + + it 'returns 400 for wrong parameters' do + delete v3_api("/projects/#{project.id}/labels", user) + expect(response).to have_http_status(400) + end + end end diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb index 28c3ca03960..13814ed10c3 100644 --- a/spec/requests/api/v3/members_spec.rb +++ b/spec/requests/api/v3/members_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Members, api: true do +describe API::V3::Members, api: true do include ApiHelpers let(:master) { create(:user) } diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index b51cb3055d5..b8f0260c6a2 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe API::V3::Notes, api: true do include ApiHelpers + let(:user) { create(:user) } let!(:project) { create(:empty_project, :public, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } @@ -373,12 +374,12 @@ describe API::V3::Notes, api: true do context 'when noteable is an Issue' do it 'deletes a note' do delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) + "notes/#{issue_note.id}", user) expect(response).to have_http_status(200) # Check if note is really deleted delete v3_api("/projects/#{project.id}/issues/#{issue.id}/"\ - "notes/#{issue_note.id}", user) + "notes/#{issue_note.id}", user) expect(response).to have_http_status(404) end @@ -392,18 +393,18 @@ describe API::V3::Notes, api: true do context 'when noteable is a Snippet' do it 'deletes a note' do delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) + "notes/#{snippet_note.id}", user) expect(response).to have_http_status(200) # Check if note is really deleted delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/#{snippet_note.id}", user) + "notes/#{snippet_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/12345", user) + "notes/12345", user) expect(response).to have_http_status(404) end @@ -412,18 +413,18 @@ describe API::V3::Notes, api: true do context 'when noteable is a Merge Request' do it 'deletes a note' do delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.id}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(200) # Check if note is really deleted delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.id}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do delete v3_api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) + "#{merge_request.id}/notes/12345", user) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 662be3f3531..34940b2f1c7 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -309,10 +309,37 @@ describe API::V3::Projects, api: true do end end - it 'creates new project without path and return 201' do - expect { post v3_api('/projects', user), name: 'foo' }. + it 'creates new project without path but with name and returns 201' do + expect { post v3_api('/projects', user), name: 'Foo Project' }. to change { Project.count }.by(1) expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-project') + end + + it 'creates new project without name but with path and returns 201' do + expect { post v3_api('/projects', user), path: 'foo_project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('foo_project') + expect(project.path).to eq('foo_project') + end + + it 'creates new project name and path and returns 201' do + expect { post v3_api('/projects', user), path: 'foo-Project', name: 'Foo Project' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + + project = Project.first + + expect(project.name).to eq('Foo Project') + expect(project.path).to eq('foo-Project') end it 'creates last project before reaching project limit' do @@ -321,7 +348,7 @@ describe API::V3::Projects, api: true do expect(response).to have_http_status(201) end - it 'does not create new project without name and return 400' do + it 'does not create new project without name or path and return 400' do expect { post v3_api('/projects', user) }.not_to change { Project.count } expect(response).to have_http_status(400) end diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb new file mode 100644 index 00000000000..ca335ce9cf0 --- /dev/null +++ b/spec/requests/api/v3/runners_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe API::V3::Runners, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:project) { create(:empty_project, creator_id: user.id) } + let(:project2) { create(:empty_project, creator_id: user.id) } + + let!(:shared_runner) { create(:ci_runner, :shared) } + let!(:unused_specific_runner) { create(:ci_runner) } + + let!(:specific_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + end + end + + let!(:two_projects_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + create(:ci_runner_project, runner: runner, project: project2) + end + end + + before do + # Set project access for users + create(:project_member, :master, user: user, project: project) + create(:project_member, :reporter, user: user2, project: project) + end + + describe 'DELETE /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'deletes runner' do + expect do + delete v3_api("/runners/#{shared_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.shared.count }.by(-1) + end + end + + context 'when runner is not shared' do + it 'deletes unused runner' do + expect do + delete v3_api("/runners/#{unused_specific_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + + it 'deletes used runner' do + expect do + delete v3_api("/runners/#{specific_runner.id}", admin) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + end + + it 'returns 404 if runner does not exists' do + delete v3_api('/runners/9999', admin) + + expect(response).to have_http_status(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'does not delete runner' do + delete v3_api("/runners/#{shared_runner.id}", user) + expect(response).to have_http_status(403) + end + end + + context 'when runner is not shared' do + it 'does not delete runner without access to it' do + delete v3_api("/runners/#{specific_runner.id}", user2) + expect(response).to have_http_status(403) + end + + it 'does not delete runner with more than one associated project' do + delete v3_api("/runners/#{two_projects_runner.id}", user) + expect(response).to have_http_status(403) + end + + it 'deletes runner for one owned project' do + expect do + delete v3_api("/runners/#{specific_runner.id}", user) + + expect(response).to have_http_status(200) + end.to change{ Ci::Runner.specific.count }.by(-1) + end + end + end + + context 'unauthorized user' do + it 'does not delete runner' do + delete v3_api("/runners/#{specific_runner.id}") + + expect(response).to have_http_status(401) + end + end + end + + describe 'DELETE /projects/:id/runners/:runner_id' do + context 'authorized user' do + context 'when runner have more than one associated projects' do + it "disables project's runner" do + expect do + delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + + expect(response).to have_http_status(200) + end.to change{ project.runners.count }.by(-1) + end + end + + context 'when runner have one associated projects' do + it "does not disable project's runner" do + expect do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user) + end.to change{ project.runners.count }.by(0) + expect(response).to have_http_status(403) + end + end + + it 'returns 404 is runner is not found' do + delete v3_api("/projects/#{project.id}/runners/9999", user) + + expect(response).to have_http_status(404) + end + end + + context 'authorized user without permissions' do + it "does not disable project's runner" do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthorized user' do + it "does not disable project's runner" do + delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}") + + expect(response).to have_http_status(401) + end + end + end +end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb new file mode 100644 index 00000000000..7e8c8753d02 --- /dev/null +++ b/spec/requests/api/v3/services_spec.rb @@ -0,0 +1,22 @@ +require "spec_helper" + +describe API::V3::Services, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + + Service.available_services_names.each do |service| + describe "DELETE /projects/:id/services/#{service.dasherize}" do + include_context service + + it "deletes #{service}" do + delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) + + expect(response).to have_http_status(200) + project.send(service_method).reload + expect(project.send(service_method).activated?).to be_falsey + end + end + end +end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb index da58efb6ebf..91038977c82 100644 --- a/spec/requests/api/v3/system_hooks_spec.rb +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -38,4 +38,20 @@ describe API::V3::SystemHooks, api: true do end end end + + describe "DELETE /hooks/:id" do + it "deletes a hook" do + expect do + delete v3_api("/hooks/#{hook.id}", admin) + + expect(response).to have_http_status(200) + end.to change { SystemHook.count }.by(-1) + end + + it 'returns 404 if the system hook does not exist' do + delete v3_api('/hooks/12345', admin) + + expect(response).to have_http_status(404) + end + end end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb index 6722789d928..6870cfd2668 100644 --- a/spec/requests/api/v3/tags_spec.rb +++ b/spec/requests/api/v3/tags_spec.rb @@ -64,4 +64,26 @@ describe API::V3::Tags, api: true do end end end + + describe 'DELETE /projects/:id/repository/tags/:tag_name' do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + + before do + allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) + end + + context 'delete tag' do + it 'deletes an existing tag' do + delete v3_api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + + expect(response).to have_http_status(200) + expect(json_response['tag_name']).to eq(tag_name) + end + + it 'raises 404 if the tag does not exist' do + delete v3_api("/projects/#{project.id}/repository/tags/foobar", user) + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb new file mode 100644 index 00000000000..721ce4a361b --- /dev/null +++ b/spec/requests/api/v3/triggers_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe API::V3::Triggers do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } + let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + + describe 'DELETE /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'deletes trigger' do + expect do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response).to have_http_status(200) + end.to change{project.triggers.count}.by(-1) + end + + it 'responds with 404 Not Found if requesting non-existing trigger' do + delete v3_api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response).to have_http_status(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'does not delete trigger' do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response).to have_http_status(403) + end + end + + context 'unauthenticated user' do + it 'does not delete trigger' do + delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response).to have_http_status(401) + end + end + end +end diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index 769f04c5057..0c1413119e0 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -152,8 +152,9 @@ describe API::Variables, api: true do it 'deletes variable' do expect do delete api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response).to have_http_status(204) end.to change{project.variables.count}.by(-1) - expect(response).to have_http_status(200) end it 'responds with 404 Not Found if requesting non-existing variable' do diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 32c2ed8cae7..98c560ffb26 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do let!(:user) { create(:user) } let!(:group) { create(:group) } + let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:project, namespace: group) } let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } @@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do end it { expect(Group.unscoped.all).not_to include(group) } + it { expect(Group.unscoped.all).not_to include(nested_group) } it { expect(Project.unscoped.all).not_to include(project) } end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index f92978a33a3..0ff6e8fda16 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -111,6 +111,31 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end + + context 'when the merge request is not mergeable' do + let(:mr_conflict) do + create(:merge_request, merge_when_build_succeeds: true, merge_user: user, + source_branch: 'master', target_branch: 'feature-conflict', + source_project: project, target_project: project) + end + + let(:conflict_pipeline) do + create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, + sha: mr_conflict.diff_head_sha, status: 'success') + end + + it 'does not merge the merge request' do + expect(MergeWorker).not_to receive(:perform_async) + + service.trigger(conflict_pipeline) + end + + it 'creates todos for unmergeability' do + expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict) + + service.trigger(conflict_pipeline) + end + end end describe "#cancel" do diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index c3b468ac47f..d33535d22af 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -66,13 +66,12 @@ describe MergeRequests::ResolveService do context 'when the source project is a fork and does not contain the HEAD of the target branch' do let!(:target_head) do - project.repository.commit_file( + project.repository.create_file( user, 'new-file-in-target', '', message: 'Add new file in target', - branch_name: 'conflict-start', - update: false) + branch_name: 'conflict-start') end before do diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 690fe979492..08733d6dcf1 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -131,6 +131,80 @@ describe Users::RefreshAuthorizedProjectsService do it 'sets the values to the access levels' do expect(hash.values).to eq([Gitlab::Access::MASTER]) end + + context 'personal projects' do + it 'includes the project with the right access level' do + expect(hash[project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects the user is a member of' do + let!(:other_project) { create(:empty_project) } + + before do + other_project.team.add_reporter(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER) + end + end + + context 'projects of groups the user is a member of' do + let(:group) { create(:group) } + let!(:other_project) { create(:project, group: group) } + + before do + group.add_owner(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER) + end + end + + context 'projects of subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let!(:other_project) { create(:project, group: nested_group) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::MASTER) + end + end + + context 'projects shared with groups the user is a member of' do + let(:group) { create(:group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST) + end + end + + context 'projects shared with subgroups of groups the user is a member of' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:other_project) { create(:empty_project) } + let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) } + + before do + group.add_master(user) + end + + it 'includes the project with the right access level' do + expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER) + end + end end describe '#current_authorizations_per_project' do diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 6ed55289ed9..c864a705ca4 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -9,14 +9,7 @@ module CycleAnalyticsHelpers commit_shas = Array.new(count) do |index| filename = random_git_name - options = { - committer: project.repository.user_to_committer(user), - author: project.repository.user_to_committer(user), - commit: { message: message, branch: branch_name, update_ref: true }, - file: { content: "content", path: filename, update: false } - } - - commit_sha = Gitlab::Git::Blob.commit(project.repository, options) + commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name) project.repository.commit(commit_sha) commit_sha @@ -35,13 +28,12 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file( + sha = project.repository.create_file( user, random_git_name, 'content', message: 'commit message', - branch_name: source_branch, - update: false) + branch_name: source_branch) project.repository.commit(sha) opts = { diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb new file mode 100644 index 00000000000..984ec7d2741 --- /dev/null +++ b/spec/support/dropzone_helper.rb @@ -0,0 +1,37 @@ +module DropzoneHelper + # Provides a way to perform `attach_file` for a Dropzone-based file input + # + # This is accomplished by creating a standard HTML file input on the page, + # performing `attach_file` on that field, and then triggering the appropriate + # Dropzone events to perform the actual upload. + # + # This method waits for the upload to complete before returning. + def dropzone_file(file_path) + # Generate a fake file input that Capybara can attach to + page.execute_script <<-JS.strip_heredoc + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + + window._dropzoneComplete = false; + JS + + # Attach the file to the fake input selector with Capybara + attach_file('fakeFileInput', file_path) + + # Manually trigger a Dropzone "drop" event with the fake input's file list + page.execute_script <<-JS.strip_heredoc + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + + var dropzone = $('.div-dropzone')[0].dropzone; + dropzone.on('queuecomplete', function() { + window._dropzoneComplete = true; + }); + dropzone.listeners[0].events.drop(e); + JS + + # Wait until Dropzone's fired `queuecomplete` + loop until page.evaluate_script('window._dropzoneComplete === true') + end +end diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb new file mode 100644 index 00000000000..365c34448ac --- /dev/null +++ b/spec/support/update_invalid_issuable.rb @@ -0,0 +1,57 @@ +shared_examples 'update invalid issuable' do |klass| + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: issuable.iid + } + end + + let(:issuable) do + klass == Issue ? issue : merge_request + end + + before do + if klass == Issue + params.merge!(issue: { title: "any" }) + else + params.merge!(merge_request: { title: "any" }) + end + end + + context 'when updating causes conflicts' do + before do + allow_any_instance_of(issuable.class).to receive(:save). + and_raise(ActiveRecord::StaleObjectError.new(issuable, :save)) + end + + it 'renders edit when format is html' do + put :update, params + + expect(response).to render_template(:edit) + expect(assigns[:conflict]).to be_truthy + end + + it 'renders json error message when format is json' do + params[:format] = "json" + + put :update, params + + expect(response.status).to eq(409) + expect(JSON.parse(response.body)).to have_key('errors') + end + end + + context 'when updating an invalid issuable' do + before do + key = klass == Issue ? :issue : :merge_request + params[key][:title] = "" + end + + it 'renders edit when merge request is invalid' do + put :update, params + + expect(response).to render_template(:edit) + end + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index 6098be5cd45..ea714fb08f0 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AttachmentUploader do - let(:issue) { build(:issue) } - subject { described_class.new(issue) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is true' do - expect(subject.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(subject.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index 76f5a4b42ed..c4d558805ab 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe AvatarUploader do - let(:user) { build(:user) } - subject { described_class.new(user) } + let(:uploader) { described_class.new(build_stubbed(:user)) } describe '#move_to_cache' do it 'is false' do - expect(subject.move_to_cache).to eq(false) + expect(uploader.move_to_cache).to eq(false) end end describe '#move_to_store' do it 'is false' do - expect(subject.move_to_store).to eq(false) + expect(uploader.move_to_store).to eq(false) end end end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 6a712e33c96..b0f5be55c33 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,57 +1,35 @@ require 'spec_helper' describe FileUploader do - let(:project) { create(:project) } + let(:uploader) { described_class.new(build_stubbed(:project)) } - before do - @previous_enable_processing = FileUploader.enable_processing - FileUploader.enable_processing = false - @uploader = FileUploader.new(project) - end - - after do - FileUploader.enable_processing = @previous_enable_processing - @uploader.remove! - end + describe 'initialize' do + it 'generates a secret if none is provided' do + expect(SecureRandom).to receive(:hex).and_return('secret') - describe '#image_or_video?' do - context 'given an image file' do - before do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) - end + uploader = described_class.new(double) - it 'detects an image based on file extension' do - expect(@uploader.image_or_video?).to be true - end + expect(uploader.secret).to eq 'secret' end - context 'given an video file' do - before do - video_file = fixture_file_upload(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) - @uploader.store!(video_file) - end - - it 'detects a video based on file extension' do - expect(@uploader.image_or_video?).to be true - end - end + it 'accepts a secret parameter' do + expect(SecureRandom).not_to receive(:hex) - it 'does not return image_or_video? for other types' do - @uploader.store!(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + uploader = described_class.new(double, 'secret') - expect(@uploader.image_or_video?).to be false + expect(uploader.secret).to eq 'secret' end end describe '#move_to_cache' do it 'is true' do - expect(@uploader.move_to_cache).to eq(true) + expect(uploader.move_to_cache).to eq(true) end end describe '#move_to_store' do it 'is true' do - expect(@uploader.move_to_store).to eq(true) + expect(uploader.move_to_store).to eq(true) end end end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb new file mode 100644 index 00000000000..e9efd13b9aa --- /dev/null +++ b/spec/uploaders/uploader_helper_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe UploaderHelper do + class ExampleUploader < CarrierWave::Uploader::Base + include UploaderHelper + + storage :file + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + describe '#image_or_video?' do + let(:uploader) { ExampleUploader.new } + + it 'returns true for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).to be_image_or_video + end + + it 'it returns true for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).to be_image_or_video + end + + it 'returns false for other extensions' do + uploader.store!(upload_fixture('doc_sample.txt')) + + expect(uploader).not_to be_image_or_video + end + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index b6f6e7b7a2b..ec78ac30593 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -209,6 +209,10 @@ describe 'projects/builds/show', :view do it 'does not show retry button' do expect(rendered).not_to have_link('Retry') end + + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end end context 'when job is not running' do @@ -220,6 +224,23 @@ describe 'projects/builds/show', :view do it 'shows retry button' do expect(rendered).to have_link('Retry') end + + context 'if build passed' do + it 'does not show New issue button' do + expect(rendered).not_to have_link('New issue') + end + end + + context 'if build failed' do + before do + build.status = 'failed' + render + end + + it 'shows New issue button' do + expect(rendered).to have_link('New issue') + end + end end describe 'commit title in sidebar' do @@ -248,4 +269,25 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end + + describe 'New issue button' do + before do + build.status = 'failed' + render + end + + it 'links to issues/new with the title and description filled in' do + title = "Build Failed ##{build.id}" + build_url = namespace_project_build_url(project.namespace, project, build) + href = new_namespace_project_issue_path( + project.namespace, + project, + issue: { + title: title, + description: build_url + } + ) + expect(rendered).to have_link('New issue', href: href) + end + end end diff --git a/yarn.lock b/yarn.lock index 1eaa04e21c1..cb4ef36119f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4410,9 +4410,9 @@ vue-resource@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d" -vue@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde" +vue@^2.1.10: + version "2.1.10" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b" watchpack@^1.2.0: version "1.2.1" |