diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-02-06 12:08:05 +0100 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-02-06 12:08:05 +0100 |
commit | 50aec8dd0df863c6f129edb505218e744c479a4b (patch) | |
tree | 144f6d95364e9441e71a9fafe028fa5621a61cc4 | |
parent | 16a72896a21ef065a0b259d32a43dd6554cee1d2 (diff) | |
parent | 572fb0be9b1d45437b7c0ed1000399657f471ec7 (diff) | |
download | gitlab-ce-50aec8dd0df863c6f129edb505218e744c479a4b.tar.gz |
Merge branch 'master' into feature/gb/paginated-environments-api
* master: (295 commits)
Add index to labels for `type` and project_id`
fix rack-proxy dependency in production
Fixed typo
fix failing test
fix Vue warnings for missing element
UX Guide: Button placement in groups
Change window size before visiting page, to get correct scroll position
Fix slash commands spec error
Move project services to new location under Integrations
Move webhooks to new a location under Integrations
Fixed eslint test failure
Fixed adding to list bug
Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
Fixed modal lists dropdown not updating when list is deleted
Fixed remove btn error after creating new issue in list
Removed duplicated test
Removed Masonry, instead uses groups of data
Uses mixins for repeated functions
Fixed up specs
Props use objects with required & type values
...
629 files changed, 9482 insertions, 4590 deletions
diff --git a/.eslintignore b/.eslintignore index b4bfa5a1f7a..c742b08c005 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,9 @@ +/builds/ /coverage/ /coverage-javascript/ /node_modules/ /public/ /tmp/ /vendor/ -/builds/ +karma.config.js +webpack.config.js diff --git a/.eslintrc b/.eslintrc index 9ab0145820d..1a2cd821af7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,8 @@ ], "rules": { "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"], - "no-multiple-empty-lines": ["error", { "max": 1 }] + "no-multiple-empty-lines": ["error", { "max": 1 }], + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": "off" } } diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index deb5345d3bd..e2141716311 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -107,11 +107,13 @@ setup-test-env: <<: *dedicated-runner stage: prepare script: - - bundle exec rake gitlab:assets:compile 2>/dev/null + - npm install + - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: expire_in: 7d paths: + - node_modules - public/assets - tmp/tests @@ -232,7 +234,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 script: - bundle exec $CI_BUILD_NAME -rubocop: +rubocop: <<: *ruby-static-analysis <<: *dedicated-runner stage: test @@ -291,18 +293,17 @@ rake db:seed_fu: paths: - log/development.log -teaspoon: +karma: cache: paths: - vendor/ruby - - node_modules/ + - node_modules stage: test <<: *use-db <<: *dedicated-runner script: - - npm install - npm link istanbul - - bundle exec rake teaspoon + - bundle exec rake karma artifacts: name: coverage-javascript expire_in: 31d @@ -444,7 +445,7 @@ pages: <<: *dedicated-runner dependencies: - coverage - - teaspoon + - karma - lint:javascript:report script: - mv public/ .public/ diff --git a/.rubocop.yml b/.rubocop.yml index bf2b2d8afc2..cfff42e5c99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' + - 'node_modules/**/*' - 'db/*' - 'db/fixtures/**/*' - 'tmp/**/*' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d404f1b91df..8d1f3d3f926 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,27 @@ contributing to GitLab. Please see the [UX Guide for GitLab]. +## Release retrospective and kickoff + +### Retrospective + +After each release (usually on the 22nd of each month), we have a retrospective +call where we discuss what went well, what went wrong, and what we can improve +for the next release. The [retrospective notes] are public and you are invited +to comment them. +If you're interested, you can even join the [retrospective call][retro-kickoff-call]. + +### Kickoff + +Before working on the next release (usually on the 8th of each month), we have a +kickoff call to explain what we expect to ship in the next release. The +[kickoff notes] are public and you are invited to comment them. +If you're interested, you can even join the [kickoff call][retro-kickoff-call]. + +[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing +[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing +[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206 + ## Issue tracker To get support for your particular problem please use the @@ -7,7 +7,6 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'responders', '~> 2.0' gem 'sprockets', '~> 3.7.0' -gem 'sprockets-es6', '~> 0.9.2' # Default values for AR models gem 'default_value_for', '~> 3.0.0' @@ -109,7 +108,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'asciidoctor-plantuml', '0.0.6' +gem 'asciidoctor-plantuml', '0.0.7' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.8' @@ -219,10 +218,12 @@ gem 'oj', '~> 2.17.4' gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' +gem 'webpack-rails', '~> 0.9.9' +gem 'rack-proxy', '~> 0.6.0' + gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' @@ -292,13 +293,9 @@ group :development, :test do gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' - gem 'teaspoon', '~> 1.1.0' - gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.7.0' gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-spinach', '~> 1.1.0' - gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.46.0', require: false gem 'rubocop-rspec', '~> 1.9.1', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 3b207d19d1f..82d03a86a77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,7 +54,7 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) - asciidoctor-plantuml (0.0.6) + asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) ast (2.3.0) attr_encrypted (3.0.3) @@ -72,10 +72,6 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - babel-source (5.8.35) - babel-transpiler (0.7.0) - babel-source (>= 4.0, < 6) - execjs (~> 2.0) babosa (1.0.2) base32 (0.3.2) bcrypt (3.1.11) @@ -266,8 +262,6 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab-turbolinks-classic (2.5.6) - coffee-rails gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) @@ -548,6 +542,8 @@ GEM rack (>= 1.1) rack-protection (1.5.3) rack + rack-proxy (0.6.0) + rack rack-test (0.6.3) rack (>= 1.0) rails (4.2.7.1) @@ -735,15 +731,9 @@ GEM spring (>= 0.9.1) spring-commands-spinach (1.1.0) spring (>= 0.9.1) - spring-commands-teaspoon (0.0.2) - spring (>= 0.9.1) sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-es6 (0.9.2) - babel-source (>= 5.8.11) - babel-transpiler - sprockets (>= 3.0.0) sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) @@ -761,10 +751,6 @@ GEM sys-filesystem (1.1.6) ffi sysexits (1.2.0) - teaspoon (1.1.5) - railties (>= 3.2.5, < 6) - teaspoon-jasmine (2.2.0) - teaspoon (>= 1.0.0) temple (0.7.7) test_after_commit (1.1.0) activerecord (>= 3.2) @@ -816,6 +802,8 @@ GEM webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) + webpack-rails (0.9.9) + rails (>= 3.2.0) websocket-driver (0.6.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -841,7 +829,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - asciidoctor-plantuml (= 0.0.6) + asciidoctor-plantuml (= 0.0.7) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -891,7 +879,6 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab-turbolinks-classic (~> 2.5, >= 2.5.6) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) @@ -955,6 +942,7 @@ DEPENDENCIES rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) + rack-proxy (~> 0.6.0) rails (= 4.2.7.1) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) @@ -996,14 +984,10 @@ DEPENDENCIES spring (~> 1.7.0) spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) - spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.7.0) - sprockets-es6 (~> 0.9.2) stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) - teaspoon (~> 1.1.0) - teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 1.1) thin (~> 1.7.0) timecop (~> 0.8.0) @@ -1019,6 +1003,7 @@ DEPENDENCIES vmstat (~> 2.3.0) web-console (~> 2.0) webmock (~> 1.21.0) + webpack-rails (~> 0.9.9) wikicloth (= 0.8.1) BUNDLED WITH diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 993f427c9fb..424dc719c78 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ -/* global Turbolinks */ (function() { this.Admin = (function() { @@ -42,10 +41,10 @@ return $('.change-owner-link').show(); }); $('li.project_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); $('li.group_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); showBlacklistType = function() { if ($("input[name='blacklist_type']:checked").val() === 'file') { diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4849aab50f4..637fca4d4da 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */ /* global bp */ /* global Cookies */ /* global Flash */ @@ -6,65 +6,60 @@ /* global AwardsHandler */ /* global Aside */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -/*= require jquery2 */ -/*= require jquery-ui/autocomplete */ -/*= require jquery-ui/datepicker */ -/*= require jquery-ui/draggable */ -/*= require jquery-ui/effect-highlight */ -/*= require jquery-ui/sortable */ -/*= require jquery_ujs */ -/*= require jquery.endless-scroll */ -/*= require jquery.highlight */ -/*= require jquery.waitforimages */ -/*= require jquery.atwho */ -/*= require jquery.scrollTo */ -/*= require jquery.turbolinks */ -/*= require js.cookie */ -/*= require turbolinks */ -/*= require autosave */ -/*= require bootstrap/affix */ -/*= require bootstrap/alert */ -/*= require bootstrap/button */ -/*= require bootstrap/collapse */ -/*= require bootstrap/dropdown */ -/*= require bootstrap/modal */ -/*= require bootstrap/scrollspy */ -/*= require bootstrap/tab */ -/*= require bootstrap/transition */ -/*= require bootstrap/tooltip */ -/*= require bootstrap/popover */ -/*= require select2 */ -/*= require underscore */ -/*= require dropzone */ -/*= require mousetrap */ -/*= require mousetrap/pause */ -/*= require shortcuts */ -/*= require shortcuts_navigation */ -/*= require shortcuts_dashboard_navigation */ -/*= require shortcuts_issuable */ -/*= require shortcuts_network */ -/*= require jquery.nicescroll */ -/*= require date.format */ -/*= require_directory ./behaviors */ -/*= require_directory ./blob */ -/*= require_directory ./templates */ -/*= require_directory ./commit */ -/*= require_directory ./extensions */ -/*= require_directory ./lib/utils */ -/*= require_directory ./u2f */ -/*= require_directory ./droplab */ -/*= require_directory . */ -/*= require fuzzaldrin-plus */ -/*= require es6-promise.auto */ +function requireAll(context) { return context.keys().map(context); } + +window.$ = window.jQuery = require('jquery'); +require('jquery-ui/ui/autocomplete'); +require('jquery-ui/ui/datepicker'); +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'); +require('vendor/jquery.waitforimages'); +require('vendor/jquery.caret'); +require('vendor/jquery.atwho'); +require('vendor/jquery.scrollTo'); +window.Cookies = require('vendor/js.cookie'); +require('./autosave'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); +require('select2/select2.js'); +window._ = require('underscore'); +window.Dropzone = require('dropzone'); +require('mousetrap'); +require('mousetrap/plugins/pause/mousetrap-pause'); +require('./shortcuts'); +require('./shortcuts_navigation'); +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)$/)); +require('vendor/fuzzaldrin-plus'); +window.ES6Promise = require('vendor/es6-promise.auto'); +window.ES6Promise.polyfill(); (function () { - document.addEventListener('page:fetch', function () { + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 629dc267337..9d776b74965 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,11 +1,13 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */ /* global Cookies */ +var emojiAliases = require('emoji-aliases'); + (function() { this.AwardsHandler = (function() { var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence function AwardsHandler() { - this.aliases = gl.emojiAliases(); + this.aliases = emojiAliases; $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { return function(e) { e.stopPropagation(); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index 7e6c44fa1cd..a489523b802 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ /* global autosize */ -/*= require autosize */ +var autosize = require('vendor/autosize'); (function() { $(function() { diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index d4895011be7..7747306688c 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -6,7 +6,7 @@ // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // is submitted. // -/*= require extensions/jquery */ +require('../extensions/jquery'); // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index ccbd6b993cb..6276933e93e 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -4,7 +4,7 @@ // When called on a form with input fields with the `required` attribute, the // form's submit button will be disabled until all required fields have values. // -/*= require extensions/jquery */ +require('../extensions/jquery'); // // ### Example Markup diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index d3455fa3d8c..ec1c018424d 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,7 +1,8 @@ /* eslint-disable no-param-reassign, comma-dangle */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); + ((global) => { class BlobCiYamlSelector extends gl.TemplateSelector { requestFile(query) { diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 index bdf95017613..d4f60cc6ecd 100644 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 @@ -1,5 +1,6 @@ /* global Api */ -/*= require blob/template_selector */ + +require('./template_selector'); (() => { const global = window.gl || (window.gl = {}); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 5fd0857db29..1d0bcf6471f 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 7a14eb160d0..1d5672d4c48 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index dfad9b2122b..9e0754819fa 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -2,7 +2,7 @@ /* global EditBlob */ /* global NewCommitForm */ -/*= require_tree . */ +require('./edit_blob'); (function() { $(function() { diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index f9766471780..e3241974e59 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,23 +1,27 @@ -/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ /* global Vue */ /* global BoardService */ -//= require vue -//= require vue-resource -//= require Sortable -//= require_tree ./models -//= require_tree ./stores -//= require_tree ./services -//= require_tree ./mixins -//= require_tree ./filters -//= require ./components/board -//= require ./components/board_sidebar -//= require ./components/new_list_dropdown -//= require ./vue_resource_interceptor +function requireAll(context) { return context.keys().map(context); } + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +window.Sortable = require('vendor/Sortable'); +requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/)); +require('./components/board'); +require('./components/board_sidebar'); +require('./components/new_list_dropdown'); +require('./components/modal/index'); +require('./vue_resource_interceptor'); $(() => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; @@ -31,7 +35,8 @@ $(() => { el: $boardApp, components: { 'board': gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar + 'board-sidebar': gl.issueBoards.BoardSidebar, + 'board-add-issues-modal': gl.issueBoards.IssuesModal, }, data: { state: Store.state, @@ -40,6 +45,8 @@ $(() => { boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, detailIssue: Store.detail }, computed: { @@ -48,7 +55,7 @@ $(() => { }, }, created () { - gl.boardService = new BoardService(this.endpoint, this.boardId); + gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); }, mounted () { Store.disabled = this.disabled; @@ -59,8 +66,6 @@ $(() => { if (list.type === 'done') { list.position = Infinity; - } else if (list.type === 'backlog') { - list.position = -1; } }); @@ -73,7 +78,7 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: '#js-boards-search', + el: document.getElementById('js-boards-search'), data: { filters: Store.state.filters }, @@ -81,4 +86,27 @@ $(() => { gl.issueBoards.newListDropdownInit(); } }); + + gl.IssueBoardsModalAddBtn = new Vue({ + mixins: [gl.issueBoards.ModalMixins], + el: document.getElementById('js-add-issues-btn'), + data: { + modal: ModalStore.store, + store: Store.state, + }, + computed: { + disabled() { + return Store.shouldAddBlankState(); + }, + }, + template: ` + <button + class="btn btn-create pull-right prepend-left-10 has-tooltip" + type="button" + :disabled="disabled" + @click="toggleModal(true)"> + Add issues + </button> + `, + }); }); diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index a32881116d5..18324de18b3 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -2,9 +2,9 @@ /* global Vue */ /* global Sortable */ -//= require ./board_blank_state -//= require ./board_delete -//= require ./board_list +require('./board_blank_state'); +require('./board_delete'); +require('./board_list'); (() => { const Store = gl.issueBoards.BoardsStore; @@ -22,7 +22,8 @@ props: { list: Object, disabled: Boolean, - issueLinkBase: String + issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 5fc50280811..0ea66bd027c 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,6 +1,8 @@ /* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ /* global Vue */ +require('./issue_card_inner'); + (() => { const Store = gl.issueBoards.BoardsStore; @@ -9,12 +11,16 @@ gl.issueBoards.BoardCard = Vue.extend({ template: '#js-board-list-card', + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { list: Object, issue: Object, issueLinkBase: String, disabled: Boolean, - index: Number + index: Number, + rootPath: String, }, data () { return { @@ -28,31 +34,6 @@ } }, methods: { - filterByLabel (label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters['label_name'].indexOf(label.title); - $(e.target).tooltip('hide'); - - if (labelIndex === -1) { - Store.state.filters['label_name'].push(label.title); - $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); - } else { - Store.state.filters['label_name'].splice(labelIndex, 1); - labelToggleText = Store.state.filters['label_name'][0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); - } - - const selectedLabels = Store.state.filters['label_name']; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); - - Store.updateFiltersUrl(); - }, mouseDown () { this.showDetail = true; }, @@ -71,6 +52,7 @@ Store.detail.issue = {}; } else { Store.detail.issue = this.issue; + Store.detail.list = this.list; } } } diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 630fe084175..60b0a30af3f 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -2,8 +2,8 @@ /* global Vue */ /* global Sortable */ -//= require ./board_card -//= require ./board_new_issue +require('./board_card'); +require('./board_new_issue'); (() => { const Store = gl.issueBoards.BoardsStore; @@ -23,6 +23,7 @@ issues: Array, loading: Boolean, issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index 2386d3a613c..b5c14a198ba 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -37,6 +37,7 @@ $(this.$refs.submitButton).enable(); Store.detail.issue = issue; + Store.detail.list = this.list; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 75dfcb66bb0..dfc6eed785c 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -5,6 +5,8 @@ /* global LabelsSelect */ /* global Sidebar */ +require('./sidebar/remove_issue'); + (() => { const Store = gl.issueBoards.BoardsStore; @@ -18,7 +20,8 @@ data() { return { detail: Store.detail, - issue: {} + issue: {}, + list: {}, }; }, computed: { @@ -36,6 +39,7 @@ } this.issue = this.detail.issue; + this.list = this.detail.list; }, deep: true }, @@ -60,6 +64,9 @@ new LabelsSelect(); new Sidebar(); gl.Subscription.bindAll('.subscription'); - } + }, + components: { + removeBtn: gl.issueBoards.RemoveIssueBtn, + }, }); })(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 new file mode 100644 index 00000000000..22a8b971ff8 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -0,0 +1,111 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssueCardInner = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + }, + rootPath: { + type: String, + required: true, + }, + }, + methods: { + showLabel(label) { + if (!this.list) return true; + + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + let labelToggleText = label.title; + const labelIndex = Store.state.filters.label_name.indexOf(label.title); + $(e.currentTarget).tooltip('hide'); + + if (labelIndex === -1) { + Store.state.filters.label_name.push(label.title); + $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); + } else { + Store.state.filters.label_name.splice(labelIndex, 1); + labelToggleText = Store.state.filters.label_name[0]; + $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + } + + const selectedLabels = Store.state.filters.label_name; + if (selectedLabels.length === 0) { + labelToggleText = 'Label'; + } else if (selectedLabels.length > 1) { + labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; + } + + $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + + Store.updateFiltersUrl(); + }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, + }, + template: ` + <div> + <h4 class="card-title"> + <i + class="fa fa-eye-slash confidential-icon" + v-if="issue.confidential"></i> + <a + :href="issueLinkBase + '/' + issue.id" + :title="issue.title"> + {{ issue.title }} + </a> + </h4> + <div class="card-footer"> + <span + class="card-number" + v-if="issue.id"> + #{{ issue.id }} + </span> + <a + class="card-assignee has-tooltip" + :href="rootPath + issue.assignee.username" + :title="'Assigned to ' + issue.assignee.name" + v-if="issue.assignee" + data-container="body"> + <img + class="avatar avatar-inline s20" + :src="issue.assignee.avatar" + width="20" + height="20" + :alt="'Avatar for ' + issue.assignee.name" /> + </a> + <button + class="label color-label has-tooltip" + v-for="label in issue.labels" + type="button" + v-if="showLabel(label)" + @click="filterByLabel(label, $event)" + :style="labelStyle(label)" + :title="label.description" + data-container="body"> + {{ label.title }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 new file mode 100644 index 00000000000..9538f5b69e9 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -0,0 +1,70 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + props: { + image: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + }, + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; + + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to <strong>All issues</strong> and select some issues + to add to your board. + `; + } + + return obj; + }, + }, + template: ` + <section class="empty-state"> + <div class="row"> + <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <aside class="svg-content" v-html="image"></aside> + </div> + <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="text-content"> + <h4>{{ contents.title }}</h4> + <p v-html="contents.content"></p> + <a + :href="newIssuePath" + class="btn btn-success btn-inverted" + v-if="activeTab === 'all'"> + New issue + </a> + <button + type="button" + class="btn btn-default" + @click="changeTab('all')" + v-if="activeTab === 'selected'"> + All issues + </button> + </div> + </div> + </div> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 new file mode 100644 index 00000000000..1cbc422c961 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -0,0 +1,83 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global Flash */ + +require('./lists_dropdown'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooter = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + submitDisabled() { + return !ModalStore.selectedCount(); + }, + submitText() { + const count = ModalStore.selectedCount(); + + return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; + }, + }, + methods: { + addIssues() { + const list = this.modal.selectedList || this.state.lists[0]; + const selectedIssues = ModalStore.getSelectedIssues(); + const issueIds = selectedIssues.map(issue => issue.globalId); + + // Post the data to the backend + gl.boardService.bulkUpdate(issueIds, { + add_label_ids: [list.label.id], + }).catch(() => { + new Flash('Failed to update issues, please try again.', 'alert'); + + selectedIssues.forEach((issue) => { + list.removeIssue(issue); + list.issuesSize -= 1; + }); + }); + + // Add the issues on the frontend + selectedIssues.forEach((issue) => { + list.addIssue(issue); + list.issuesSize += 1; + }); + + this.toggleModal(false); + }, + }, + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, + template: ` + <footer + class="form-actions add-issues-footer"> + <div class="pull-left"> + <button + class="btn btn-success" + type="button" + :disabled="submitDisabled" + @click="addIssues"> + {{ submitText }} + </button> + <span class="inline add-issues-footer-to-list"> + to list + </span> + <lists-dropdown></lists-dropdown> + </div> + <button + class="btn btn-default pull-right" + type="button" + @click="toggleModal(false)"> + Cancel + </button> + </footer> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 new file mode 100644 index 00000000000..ab903722ba4 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -0,0 +1,70 @@ +/* global Vue */ + +require('./tabs'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Deselect all'; + }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, + }, + methods: { + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + }, + }, + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + }, + template: ` + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)"> + <span aria-hidden="true">×</span> + </button> + </h2> + </header> + <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> + <div + class="add-issues-search append-bottom-10" + v-if="showSearch"> + <input + placeholder="Search issues..." + class="form-control" + type="search" + v-model="searchTerm" /> + <button + type="button" + class="btn btn-success btn-inverted prepend-left-10" + ref="selectAllBtn" + @click="toggleAll"> + {{ selectAllText }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 new file mode 100644 index 00000000000..d367b7e4246 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -0,0 +1,136 @@ +/* global Vue */ +/* global ListIssue */ + +require('./header'); +require('./list'); +require('./footer'); +require('./empty_state'); + +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.IssuesModal = Vue.extend({ + props: { + blankStateImage: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + page() { + this.loadIssues(); + }, + searchTerm() { + this.searchOperation(); + }, + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; + this.issuesCount = false; + } + }, + }, + methods: { + searchOperation: _.debounce(function searchOperationDebounce() { + this.loadIssues(true); + }, 500), + loadIssues(clearIssues = false) { + return gl.boardService.getBacklog({ + search: this.searchTerm, + page: this.page, + per: this.perPage, + }).then((res) => { + const data = res.json(); + + if (clearIssues) { + this.issues = []; + } + + data.issues.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = !!foundSelectedIssue; + + this.issues.push(issue); + }); + + this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = data.size; + } + }); + }, + }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header></modal-header> + <modal-list + :issue-link-base="issueLinkBase" + :root-path="rootPath" + v-if="!loading && showList"></modal-list> + <empty-state + v-if="showEmptyState" + :image="blankStateImage" + :new-issue-path="newIssuePath"></empty-state> + <section + class="add-issues-list text-center" + v-if="loading"> + <div class="add-issues-list-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + </section> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 new file mode 100644 index 00000000000..d0901219216 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -0,0 +1,142 @@ +/* global Vue */ +/* global ListIssue */ +/* global bp */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalList = Vue.extend({ + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return ModalStore.store; + }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + computed: { + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, + }, + methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage + && currentPage === this.page) { + this.loadingNewPage = true; + this.page += 1; + } + }, + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; + }, + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + template: ` + <section + class="add-issues-list add-issues-list-columns" + ref="list"> + <div + v-for="group in groupedIssues" + class="add-issues-list-column"> + <div + v-for="issue in group" + v-if="showIssue(issue)" + class="card-parent"> + <div + class="card" + :class="{ 'is-active': issue.selected }" + @click="toggleIssue($event, issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath"> + </issue-card-inner> + <span + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" + v-if="issue.selected" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> + </div> + </div> + </div> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 new file mode 100644 index 00000000000..3c05120a2da --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -0,0 +1,56 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ + data() { + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; + }, + computed: { + selected() { + return this.modal.selectedList || this.state.lists[0]; + }, + }, + destroyed() { + this.modal.selectedList = null; + }, + template: ` + <div class="dropdown inline"> + <button + class="dropdown-menu-toggle" + type="button" + data-toggle="dropdown" + aria-expanded="false"> + <span + class="dropdown-label-box" + :style="{ backgroundColor: selected.label.color }"> + </span> + {{ selected.title }} + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <ul> + <li + v-for="list in state.lists" + v-if="list.type == 'label'"> + <a + href="#" + role="button" + :class="{ 'is-active': list.id == selected.id }" + @click.prevent="modal.selectedList = list"> + <span + class="dropdown-label-box" + :style="{ backgroundColor: list.label.color }"> + </span> + {{ list.title }} + </a> + </li> + </ul> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 new file mode 100644 index 00000000000..e8cb43f3503 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -0,0 +1,47 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], + data() { + return ModalStore.store; + }, + computed: { + selectedCount() { + return ModalStore.selectedCount(); + }, + }, + destroyed() { + this.activeTab = 'all'; + }, + template: ` + <div class="top-area prepend-top-10 append-bottom-10"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')"> + All issues + <span class="badge"> + {{ issuesCount }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')"> + Selected issues + <span class="badge"> + {{ selectedCount }} + </span> + </a> + </li> + </ul> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 new file mode 100644 index 00000000000..e74935e1cb0 --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -0,0 +1,59 @@ +/* eslint-disable no-new */ +/* global Vue */ +/* global Flash */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.RemoveIssueBtn = Vue.extend({ + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, + methods: { + removeIssue() { + const issue = this.issue; + const lists = issue.getLists(); + const labelIds = lists.map(list => list.label.id); + + // Post the remove data + gl.boardService.bulkUpdate([issue.globalId], { + remove_label_ids: labelIds, + }).catch(() => { + new Flash('Failed to remove issue from board, please try again.', 'alert'); + + lists.forEach((list) => { + list.addIssue(issue); + }); + }); + + // Remove from the frontend store + lists.forEach((list) => { + list.removeIssue(issue); + }); + + Store.detail.issue = {}; + }, + }, + template: ` + <div + class="block list" + v-if="list.type !== 'done'"> + <button + class="btn btn-default btn-block" + type="button" + @click="removeIssue"> + Remove from board + </button> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 new file mode 100644 index 00000000000..d378b7d4baf --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 @@ -0,0 +1,14 @@ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalMixins = { + methods: { + toggleModal(toggle) { + ModalStore.store.showAddIssuesModal = toggle; + }, + changeTab(tab) { + ModalStore.store.activeTab = tab; + }, + }, + }; +})(); diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 31531c3ee34..2d0a295ae4d 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -6,12 +6,15 @@ class ListIssue { constructor (obj) { + this.globalId = obj.id; this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; this.dueDate = obj.due_date; this.subscribed = obj.subscribed; this.labels = []; + this.selected = false; + this.assignee = false; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 3dd5f273057..5152be56b66 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -9,7 +9,7 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1; + this.preset = ['done', 'blank'].indexOf(this.type) > -1; this.filters = gl.issueBoards.BoardsStore.state.filters; this.page = 1; this.loading = true; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index ea55158306b..065e90518df 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -2,7 +2,13 @@ /* global Vue */ class BoardService { - constructor (root, boardId) { + constructor (root, bulkUpdatePath, boardId) { + this.boards = Vue.resource(`${root}{/id}.json`, {}, { + issues: { + method: 'GET', + url: `${root}/${boardId}/issues.json` + } + }); this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', @@ -10,7 +16,12 @@ class BoardService { } }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + bulkUpdate: { + method: 'POST', + url: bulkUpdatePath, + }, + }); Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); @@ -65,6 +76,20 @@ class BoardService { issue }); } + + getBacklog(data) { + return this.boards.issues(data); + } + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return this.issues.bulkUpdate(data); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index cdf1b09c0a4..50842ecbaaa 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -34,15 +34,10 @@ }, new (listObj) { const list = this.addList(listObj); - const backlogList = this.findList('type', 'backlog', 'backlog'); list .save() .then(() => { - // Remove any new issues from the backlog - // as they will be visible in the new list - list.issues.forEach(backlogList.removeIssue.bind(backlogList)); - this.state.lists = _.sortBy(this.state.lists, 'position'); }); this.removeBlankState(); @@ -52,7 +47,7 @@ }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]); + return !(this.state.lists.filter(list => list.type !== 'done')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -102,7 +97,7 @@ listTo.addIssue(issue, listFrom, newIndex); } - if (listTo.type === 'done' && listFrom.type !== 'backlog') { + if (listTo.type === 'done') { issueLists.forEach((list) => { list.removeIssue(issue); }); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 new file mode 100644 index 00000000000..73518b42b84 --- /dev/null +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -0,0 +1,96 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + class ModalStore { + constructor() { + this.store = { + columns: 3, + issues: [], + issuesCount: false, + selectedIssues: [], + showAddIssuesModal: false, + activeTab: 'all', + selectedList: null, + searchTerm: '', + loading: false, + loadingNewPage: false, + page: 1, + perPage: 50, + }; + } + + selectedCount() { + return this.getSelectedIssues().length; + } + + toggleIssue(issueObj) { + const issue = issueObj; + const selected = issue.selected; + + issue.selected = !selected; + + if (!selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + + toggleAll() { + const select = this.selectedCount() !== this.store.issues.length; + + this.store.issues.forEach((issue) => { + const issueUpdate = issue; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + }); + } + + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); + } + + addSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); + + if (index === -1) { + this.store.selectedIssues.push(issue); + } + } + + removeSelectedIssue(issue, forcePurge = false) { + if (this.store.activeTab === 'all' || forcePurge) { + this.store.selectedIssues = this.store.selectedIssues + .filter(fIssue => fIssue.id !== issue.id); + } + } + + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); + } + + selectedIssueIndex(issue) { + return this.store.selectedIssues.indexOf(issue); + } + + findSelectedIssue(issue) { + return this.store.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + } + } + + gl.issueBoards.ModalStore = new ModalStore(); +})(); diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index eae062a3aa3..f8dac1ff56e 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -43,6 +43,7 @@ BreakpointInstance.prototype.getBreakpointSize = function() { var $visibleDevice; $visibleDevice = this.visibleDevice; + // TODO: Consider refactoring in light of turbolinks removal. // the page refreshed via turbolinks if (!$visibleDevice().length) { this.setup(); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0df84234520..0152be88b48 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ /* global Breakpoints */ -/* global Turbolinks */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -127,7 +126,7 @@ pageUrl += DOWN_BUILD_TRACE; } - return Turbolinks.visit(pageUrl); + return gl.utils.visitUrl(pageUrl); } }; })(this) diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 index b94125a4210..2bfe57b4100 100644 --- a/app/assets/javascripts/copy_as_gfm.js.es6 +++ b/app/assets/javascripts/copy_as_gfm.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ /* jshint esversion: 6 */ -/*= require lib/utils/common_utils */ +require('./lib/utils/common_utils'); (() => { const gfmRules = { diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 3485f8f91ed..0029c59e550 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */ /* global Clipboard */ -/*= require clipboard */ +window.Clipboard = require('vendor/clipboard'); (function() { var genericError, genericSuccess, showTooltip; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 2f810a69758..c41c57c1dcd 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -2,9 +2,12 @@ /* global Cookies */ /* global Flash */ -//= require vue -//= require_tree ./svg -//= require_tree . +window.Vue = require('vue'); +window.Cookies = require('vendor/js.cookie'); + +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 35a029194d0..c39e30fb7e0 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -1,9 +1,10 @@ /* eslint-disable class-methods-use-this */ -//= require lib/utils/url_utility */ +require('./lib/utils/url_utility'); (() => { const UNFOLD_COUNT = 20; + let isBound = false; class Diff { constructor() { @@ -17,10 +18,12 @@ $('.content-wrapper .container-fluid').removeClass('container-limited'); } - $(document) - .off('click', '.js-unfold, .diff-line-num a') - .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) - .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + if (!isBound) { + $(document) + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + isBound = true; + } this.openAnchoredDiff(); } diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index 1b3a57d0962..f0edfb8aaf1 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,12 +1,13 @@ -/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ /* global Vue */ /* global ResolveCount */ -//= require_directory ./models -//= require_directory ./stores -//= require_directory ./services -//= require_directory ./mixins -//= require_directory ./components +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); $(() => { const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 3d183f4ecb4..a510eebae1a 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ /* global Dropzone */ -/*= require preview_markdown */ +require('./preview_markdown'); (function() { this.DropzoneInput = (function() { diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 971be04e2d2..91553bda4dc 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -3,10 +3,10 @@ /* global EnvironmentsService */ /* global Flash */ -//= require vue -//= require vue-resource -//= require_tree ../services/ -//= require ./environment_item +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../services/environments_service'); +require('./environment_item'); (() => { window.gl = window.gl || {}; @@ -180,7 +180,7 @@ <tr> <th class="environments-name">Environment</th> <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Build</th> + <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> diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 81468f4d3bc..ed1c78945db 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index 6592c1b5f0f..28cc0022d17 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0e6bc3fdb2c..521873b14b4 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,14 +1,15 @@ /* global Vue */ /* global timeago */ -/*= require timeago */ -/*= require lib/utils/text_utility */ -/*= require vue_common_component/commit */ -/*= require ./environment_actions */ -/*= require ./environment_external_url */ -/*= require ./environment_stop */ -/*= require ./environment_rollback */ -/*= require ./environment_terminal_button */ +window.Vue = require('vue'); +window.timeago = require('vendor/timeago'); +require('../../lib/utils/text_utility'); +require('../../vue_common_component/commit'); +require('./environment_actions'); +require('./environment_external_url'); +require('./environment_stop'); +require('./environment_rollback'); +require('./environment_terminal_button'); (() => { /** diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index b52298b4a88..5938340a128 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index 0a29f2f36e9..be9526989a0 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index 050184ba497..a3ad063f7cb 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 3b003f6f661..58f4c6eadb2 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,7 +1,8 @@ -//= require vue -//= require_tree ./stores/ -//= require ./components/environment -//= require ./vue_resource_interceptor +window.Vue = require('vue'); + +require('./stores/environments_store'); +require('./components/environment'); +require('./vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 575a45d9802..fab8d977f58 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -1,5 +1,6 @@ /* globals Vue */ /* eslint-disable no-unused-vars, no-param-reassign */ + class EnvironmentsService { constructor(root) { diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 index cd401277689..f8256a8d26d 100644 --- a/app/assets/javascripts/extensions/array.js.es6 +++ b/app/assets/javascripts/extensions/array.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */ +/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */ + +'use strict'; + Array.prototype.first = function() { return this[0]; }; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 7d297b8eee8..572c221929a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -1,4 +1,4 @@ -/*= require filtered_search/filtered_search_dropdown */ +require('./filtered_search_dropdown'); /* global droplabFilter */ diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 13cbec1be4a..b3dc3e502c5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -1,4 +1,4 @@ -/*= require filtered_search/filtered_search_dropdown */ +require('./filtered_search_dropdown'); /* global droplabAjax */ /* global droplabFilter */ diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 162fd6044e5..f93605a5a21 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -1,4 +1,4 @@ -/*= require filtered_search/filtered_search_dropdown */ +require('./filtered_search_dropdown'); /* global droplabAjaxFilter */ diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index d188718c5f3..392f1835966 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,7 +1,3 @@ - // This is a manifest file that'll be compiled into including all the files listed below. - // Add new JavaScript code in separate files in this directory and they'll automatically - // be included in the compiled file accessible from http://example.com/assets/application.js - // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the - // the compiled file. - // - /*= require_tree . */ +function requireAll(context) { return context.keys().map(context); } + +requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 00e1c28692f..547989a6ff5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -9,7 +9,7 @@ this.setupMapping(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } cleanup() { @@ -20,7 +20,7 @@ this.setupMapping(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } setupMapping() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 029564ffc61..4e02ab7c8c1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,3 @@ -/* global Turbolinks */ - (() => { class FilteredSearchManager { constructor() { @@ -15,13 +13,13 @@ this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } } cleanup() { this.unbindEvents(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } bindEvents() { @@ -200,7 +198,9 @@ paths.push(`search=${sanitized}`); } - Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + + gl.utils.visitUrl(parameterizedUrl); } getUsernameParams() { diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3f23095dad9..7f1f2a5d278 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -83,12 +83,12 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); match = regexp.exec(subtext); if (match) { - return match[2] || match[1]; + return (match[1] || match[1] === "") ? match[1] : match[2]; } else { return null; } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d2f66cf5249..d9101b55c7f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -/* global Turbolinks */ (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, @@ -249,7 +248,7 @@ _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') { + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } }; @@ -723,7 +722,7 @@ if ($el.length) { var href = $el.attr('href'); if (href && href !== '#') { - Turbolinks.visit(href); + gl.utils.visitUrl(href); } else { $el.first().trigger('click'); } diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 16be930a2f4..e9add115429 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ -//= require gl_field_error +require('./gl_field_error'); ((global) => { const customValidationFlag = 'gl-field-error-ignore'; diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 32c26349da0..4f7777aa5bc 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,12 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -/*= require_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 73715286c4a..d06a1a5dae4 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -5,7 +5,7 @@ /* global ContributorsStatGraphUtil */ /* global d3 */ -/*= require d3 */ +window.d3 = require('d3'); (function() { this.ContributorsStatGraph = (function() { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index cacfc177fc8..241249fae63 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -2,7 +2,7 @@ /* global d3 */ /* global ContributorsGraph */ -/*= require d3 */ +window.d3 = require('d3'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index f63d700fd65..8df86f68218 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global Issuable */ -/* global Turbolinks */ ((global) => { var issuable_created; @@ -119,7 +118,7 @@ issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += formData; - return Turbolinks.visit(issuesUrl); + return gl.utils.visitUrl(issuesUrl); }; })(this), initResetFilters: function() { @@ -130,7 +129,7 @@ const baseIssuesUrl = target.href; $form.attr('action', baseIssuesUrl); - Turbolinks.visit(baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); }); }, initChecks: function() { diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 index 7d0465aa8b4..e927cc0077c 100644 --- a/app/assets/javascripts/issuable/issuable_bundle.js.es6 +++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6 @@ -1 +1 @@ -//= require ./time_tracking/time_tracking_bundle +require('./time_tracking/time_tracking_bundle'); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 index 72433df2818..bf27fbac5d7 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -1,5 +1,5 @@ /* global Vue */ -//= require lib/utils/pretty_time +require('../../../lib/utils/pretty_time'); (() => { Vue.component('time-tracking-collapsed-state', { @@ -39,4 +39,3 @@ `, }); })(); - diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 index 6abbd5dd167..750468c679b 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 @@ -1,5 +1,5 @@ /* global Vue */ -//= require lib/utils/pretty_time +require('../../../lib/utils/pretty_time'); (() => { const prettyTime = gl.utils.prettyTime; diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 index 26563a7713b..e38f7852b1c 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 @@ -1,10 +1,11 @@ /* global Vue */ -//= require ./help_state -//= require ./collapsed_state -//= require ./spent_only_pane -//= require ./no_tracking_pane -//= require ./estimate_only_pane -//= require ./comparison_pane + +require('./help_state'); +require('./collapsed_state'); +require('./spent_only_pane'); +require('./no_tracking_pane'); +require('./estimate_only_pane'); +require('./comparison_pane'); (() => { Vue.component('issuable-time-tracker', { diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 index 0b8da2b1f4f..1ca01d3bdb9 100644 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -1,7 +1,8 @@ /* global Vue */ -//= require ./components/time_tracker -//= require smart_interval -//= require subbable_resource + +require('./components/time_tracker'); +require('../../smart_interval'); +require('../../subbable_resource'); (() => { /* This Vue instance represents what will become the parent instance for the diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 081b0d8b0d7..6c08b1b8e61 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* global Flash */ -/*= require flash */ -/*= require jquery.waitforimages */ -/*= require task_list */ +require('./flash'); +require('vendor/jquery.waitforimages'); +require('vendor/task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index d8ad5aaeffe..9b011d89e93 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -1,7 +1,3 @@ /* eslint-disable func-names, space-before-function-paren */ -/*= require Chart */ - -(function() { - -}).call(this); +window.Chart = require('vendor/Chart'); diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index 57e7986576c..a9dd32edbed 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -1,7 +1,3 @@ /* eslint-disable func-names, space-before-function-paren */ -/*= require d3 */ - -(function() { - -}).call(this); +window.d3 = require('d3'); diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 index e810ee85bd3..2955bda1a36 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -95,7 +95,6 @@ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); return newState; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 3ed8bfd5651..5128ffd8c6f 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,8 +2,8 @@ /* global timeago */ /* global dateFormat */ -/*= require timeago */ -/*= require date.format */ +window.timeago = require('vendor/timeago'); +window.dateFormat = require('vendor/date.format'); (function() { (function(w) { diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb deleted file mode 100644 index aeb86c9fa5b..00000000000 --- a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb +++ /dev/null @@ -1,6 +0,0 @@ -(function() { - gl.emojiAliases = function() { - return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>'); - }; - -}).call(this); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 6bb575059b7..d9370db0cf2 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -161,6 +161,9 @@ gl.text.humanize = function(string) { return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); }; + gl.text.pluralize = function(str, count) { + return str + (count > 1 || count === 0 ? 's' : ''); + }; return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js.es6 index 8e15bf0735c..a1558b371f0 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js.es6 @@ -76,5 +76,11 @@ hashIndex = url.indexOf('#'); return hashIndex === -1 ? null : url.substring(hashIndex + 1); }; + + w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); + + w.gl.utils.visitUrl = (url) => { + document.location.href = url; + }; })(window); }).call(this); diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 index eff1dcabfa2..49babdea2e1 100644 --- a/app/assets/javascripts/lib/vue_resource.js.es6 +++ b/app/assets/javascripts/lib/vue_resource.js.es6 @@ -1,2 +1,2 @@ -//= require vue -//= require vue-resource +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 2f147704c22..d7137ec63e4 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -4,7 +4,7 @@ // // Handles single- and multi-line selection and highlight for blob views. // -/*= require jquery.scrollTo */ +require('vendor/jquery.scrollTo'); // // ### Example Markup @@ -171,7 +171,6 @@ // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { return history.pushState({ - turbolinks: false, url: value // We're using pushState instead of assigning location.hash directly to // prevent the page from scrolling on the hashchange event diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index ea9bfb4860a..1b0d0768db8 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,14 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ -/* global Turbolinks */ (function() { - Turbolinks.enableProgressBar(); - - $(document).on('page:fetch', function() { + window.addEventListener('beforeunload', function() { $('.tanuki-logo').addClass('animate'); }); - - $(document).on('page:change', function() { - $('.tanuki-logo').removeClass('animate'); - }); }).call(this); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index a2d90f9ba47..653e52fb6bf 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -2,14 +2,14 @@ /* global Vue */ /* global Flash */ -//= require vue -//= require ./merge_conflict_store -//= require ./merge_conflict_service -//= require ./mixins/line_conflict_utils -//= require ./mixins/line_conflict_actions -//= require ./components/diff_file_editor -//= require ./components/inline_conflict_lines -//= require ./components/parallel_conflict_lines +window.Vue = require('vue'); +require('./merge_conflict_store'); +require('./merge_conflict_service'); +require('./mixins/line_conflict_utils'); +require('./mixins/line_conflict_actions'); +require('./components/diff_file_editor'); +require('./components/inline_conflict_lines'); +require('./components/parallel_conflict_lines'); $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 09ee8dbe9d7..8762ec35b80 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ /* global MergeRequestTabs */ -/*= require jquery.waitforimages */ -/*= require task_list */ -/*= require merge_request_tabs */ +require('vendor/jquery.waitforimages'); +require('vendor/task_list'); +require('./merge_request_tabs'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -110,9 +110,8 @@ }; MergeRequest.prototype.initCommitMessageListeners = function() { - var textarea = $('textarea.js-commit-message'); - - $('a.js-with-description-link').on('click', function(e) { + $(document).on('click', 'a.js-with-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); @@ -120,7 +119,8 @@ $('p.js-without-description-hint').show(); }); - $('a.js-without-description-link').on('click', function(e) { + $(document).on('click', 'a.js-without-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 4c8c28af755..7e74bebb81e 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -1,11 +1,11 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ /* global Cookies */ -/* global DiffNotesApp */ /* global Flash */ -/*= require js.cookie */ -/*= require breakpoints */ +require('./breakpoints'); +window.Cookies = require('vendor/js.cookie'); +require('./flash'); /* eslint-disable max-len */ // MergeRequestTabs @@ -184,12 +184,13 @@ // Ensure parameters and hash come along for the ride newState += location.search + location.hash; + // TODO: Consider refactoring in light of turbolinks removal. + // Replace the current history state with the new one without breaking // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 window.history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7cc319e2f4e..05b9a63765f 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -2,7 +2,8 @@ /* global notify */ /* global notifyPermissions */ /* global merge_request_widget */ -/* global Turbolinks */ + +require('./smart_interval'); ((global) => { var 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; }; @@ -69,13 +70,13 @@ } MergeRequestWidget.prototype.clearEventListeners = function() { - return $(document).off('page:change.merge_request'); + return $(document).off('DOMContentLoaded'); }; MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'pipelines', 'changes']; - $(document).on('page:change.merge_request', (function(_this) { + $(document).on('DOMContentLoaded', (function(_this) { return function() { var page; page = $('body').data('page').split(':').last(); @@ -154,12 +155,22 @@ return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status || + data.sha !== _this.opts.ci_sha || + data.pipeline !== _this.opts.ci_pipeline) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } + if (data.pipeline) { + _this.opts.ci_pipeline = data.pipeline; + _this.updatePipelineUrls(data.pipeline); + } + if (data.sha) { + _this.opts.ci_sha = data.sha; + _this.updateCommitUrls(data.sha); + } if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { @@ -248,6 +259,16 @@ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; + MergeRequestWidget.prototype.updatePipelineUrls = function(id) { + const pipelineUrl = this.opts.pipeline_path; + $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.updateCommitUrls = function(id) { + const commitsUrl = this.opts.commits_path; + $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 5969d2ba56b..5840916846b 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -47,7 +47,7 @@ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); }); } else { - merge_request_widget.getMergeStatus(); + setTimeout(() => merge_request_widget.getMergeStatus(), 200); } }); })(); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 2e6eb83cec7..b4491354472 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -2,13 +2,9 @@ /* global Network */ /* global ShortcutsNetwork */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -/*= require_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); (function() { $(function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index c4722be3625..d108da29af7 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -4,13 +4,14 @@ /* global ResolveService */ /* global mrRefreshWidgetUrl */ -/*= require autosave */ -/*= require autosize */ -/*= require dropzone */ -/*= require dropzone_input */ -/*= require gfm_auto_complete */ -/*= require jquery.atwho */ -/*= require task_list */ +require('./autosave'); +window.autosize = require('vendor/autosize'); +window.Dropzone = require('dropzone'); +require('./dropzone_input'); +require('./gfm_auto_complete'); +require('vendor/jquery.caret'); // required by jquery.atwho +require('vendor/jquery.atwho'); +require('vendor/task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 43263368494..9203abefbbc 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */ -//= require lib/utils/bootstrap_linked_tabs +require('./lib/utils/bootstrap_linked_tabs'); ((global) => { class Pipelines { diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index f50802bdf2e..d7f3c9fd37e 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,7 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7cf630a1d76..71719917d0c 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* global Cookies */ -/* global Turbolinks */ /* global ProjectSelect */ (function() { @@ -58,6 +57,11 @@ }; Project.prototype.initRefSwitcher = function() { + var refListItem = document.createElement('li'); + var refLink = document.createElement('a'); + + refLink.href = '#'; + return $('.js-project-refs-dropdown').each(function() { var $dropdown, selected; $dropdown = $(this); @@ -67,7 +71,8 @@ return $.ajax({ url: $dropdown.data('refs-url'), data: { - ref: $dropdown.data('ref') + ref: $dropdown.data('ref'), + search: term }, dataType: "json" }).done(function(refs) { @@ -76,16 +81,29 @@ }, selectable: true, filterable: true, + filterRemote: true, filterByText: true, fieldName: $dropdown.data('field-name'), renderRow: function(ref) { - var link; + var li = refListItem.cloneNode(false); + if (ref.header != null) { - return $('<li />').addClass('dropdown-header').text(ref.header); + li.className = 'dropdown-header'; + li.textContent = ref.header; } else { - link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref); - return $('<li />').append(link); + var link = refLink.cloneNode(false); + + if (ref === selected) { + link.className = 'is-active'; + } + + link.textContent = ref; + link.dataset.ref = ref; + + li.appendChild(link); } + + return li; }, id: function(obj, $el) { return $el.attr('data-ref'); @@ -99,7 +117,7 @@ var $form = $dropdown.closest('form'); var action = $form.attr('action'); var divider = action.indexOf('?') < 0 ? '?' : '&'; - Turbolinks.visit(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } }); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 6614d8952cd..d7943959238 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ -/* global Turbolinks */ (function() { this.ProjectImport = (function() { function ProjectImport() { setTimeout(function() { - return Turbolinks.visit(location.href); + return gl.utils.visitUrl(location.href); }, 5000); } diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index 03f4531abf5..5cf28aa7a73 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -49,7 +49,7 @@ class ProtectedBranchDropdown { onClickCreateWildcard() { // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(0); + this.$dropdown.data('glDropdown').selectRowAtIndex(); } getProtectedBranches(term, callback) { diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 15b3affd469..ffb66caf5f4 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1 +1,3 @@ -/*= require_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 0caf8ba4344..bdbad93ad04 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -9,7 +9,7 @@ this.find('.js-render-math').renderMath(); }; - $(document).on('ready page:load', function() { + $(document).on('ready load', function() { return $('body').renderGFM(); }); }).call(this); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c56ee429b8e..c6d9b007ad1 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ -/* global Turbolinks */ /* global findFileURL */ (function() { @@ -23,7 +22,7 @@ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); if (typeof findFileURL !== "undefined" && findFileURL !== null) { Mousetrap.bind('t', function() { - return Turbolinks.visit(findFileURL); + return gl.utils.visitUrl(findFileURL); }); } } diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index d50ddd98de1..a3e549a2735 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -2,7 +2,7 @@ /* global Shortcuts */ /* global Mousetrap */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 603fefbf15a..7378b322426 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global Shortcuts */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 8469837533b..36e379d634d 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -/*= require shortcuts_navigation */ +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4dcc5ebe28f..b841abb754d 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */ /* global Mousetrap */ -/* global Turbolinks */ /* global ShortcutsNavigation */ /* global sidebar */ -/*= require mousetrap */ -/*= require shortcuts_navigation */ +require('mousetrap'); +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -80,7 +79,7 @@ ShortcutsIssuable.prototype.editIssue = function() { var $editBtn; $editBtn = $('.issuable-edit'); - return Turbolinks.visit($editBtn.attr('href')); + return gl.utils.visitUrl($editBtn.attr('href')); }; ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index afeda0dd5fe..cb5f2c53ea6 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global Shortcuts */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 79896e35cbb..651957f5325 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -/*= require shortcuts_navigation */ +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 05234643c18..ee172f2fa6f 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -40,7 +40,7 @@ .on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', 'html, body', (e) => this.handleClickEvent(e)) - .on('page:change', () => this.renderState()) + .on('DOMContentLoaded', () => this.renderState()) .on('todo:toggle', (e, count) => this.updateTodoCount(count)); this.renderState(); } diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 40f67637c7c..d1bdc353be2 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -89,7 +89,7 @@ destroy() { this.cancel(); document.removeEventListener('visibilitychange', this.handleVisibilityChange); - $(document).off('visibilitychange').off('page:before-unload'); + $(document).off('visibilitychange').off('beforeunload'); } /* private */ @@ -111,8 +111,9 @@ } initPageUnloadHandling() { + // TODO: Consider refactoring in light of turbolinks removal. // prevent interval continuing after page change, when kept in cache by Turbolinks - $(document).on('page:before-unload', () => this.cancel()); + $(document).on('beforeunload', () => this.cancel()); } handleVisibilityChange(e) { diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index cfb4ff82a73..64f9065be42 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,7 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ -/*= require_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); (function() { $(function() { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index b0132af70f2..e9e9aafd71a 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* global Api */ -/*= require ../blob/template_selector */ +require('../blob/template_selector'); ((global) => { class IssuableTemplateSelector extends gl.TemplateSelector { diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 index 33d2c1e1a17..13cf3a10a38 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js.es6 +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -1,7 +1,7 @@ -//= require xterm/encoding-indexes -//= require xterm/encoding -//= require xterm/xterm.js -//= require xterm/fit.js -//= require ./terminal.js +require('vendor/xterm/encoding-indexes.js'); +require('vendor/xterm/encoding.js'); +window.Terminal = require('vendor/xterm/xterm.js'); +require('vendor/xterm/fit.js'); +require('./terminal.js'); $(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 05622916ff8..96c7d927509 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */ /* global UsersSelect */ -/* global Turbolinks */ ((global) => { class Todos { @@ -34,7 +33,7 @@ $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); } @@ -142,7 +141,7 @@ }; url = gl.utils.mergeUrlParams(pageParams, url); } - return Turbolinks.visit(url); + return gl.utils.visitUrl(url); } } @@ -156,7 +155,7 @@ e.preventDefault(); return window.open(todoLink, '_blank'); } else { - return Turbolinks.visit(todoLink); + return gl.utils.visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index d124ca4f88b..b1b35fdbd6c 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ -/* global Turbolinks */ + (function() { this.TreeView = (function() { function TreeView() { @@ -15,7 +15,7 @@ e.preventDefault(); return window.open(path, '_blank'); } else { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); @@ -57,7 +57,7 @@ } else if (e.which === 13) { path = $('.tree-item.selected .tree-item-file-name a').attr('href'); if (path) { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 313fb17aee8..465618e3d53 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -149,7 +149,6 @@ content on the Users#show page. new_state = new_state.replace(/\/+$/, ''); new_state += this._location.search + this._location.hash; history.replaceState({ - turbolinks: true, url: new_state }, document.title, new_state); return new_state; diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index e7280d643d3..6e40dfdf3d8 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ /* global d3 */ -/* global dateFormat */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -33,7 +32,7 @@ date.setDate(date.getDate() + i); var day = date.getDay(); - var count = timestamps[dateFormat(date, 'yyyy-mm-dd')]; + var count = timestamps[date.format('yyyy-mm-dd')]; // Create a new group array if this is the first day of the week // or if is first object @@ -122,7 +121,7 @@ if (stamp.count > 0) { contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); } - dateText = dateFormat(date, 'mmm d, yyyy'); + dateText = date.format('mmm d, yyyy'); return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; }; })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index f50802bdf2e..4cad60a59b1 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,7 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 62a22e39a3b..4adad7bea31 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -1,5 +1,7 @@ -/*= require vue */ /* global Vue */ + +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 index 605824fa939..67c6cb73761 100644 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ b/app/assets/javascripts/vue_pagination/index.js.es6 @@ -1,6 +1,8 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign, no-plusplus */ +window.Vue = require('vue'); + ((gl) => { const PAGINATION_UI_BUTTON_LIMIT = 4; const UI_LIMIT = 6; @@ -13,6 +15,8 @@ gl.VueGlPagination = Vue.extend({ props: { + // TODO: Consider refactoring in light of turbolinks removal. + /** This function will take the information given by the pagination component And make a new Turbolinks call @@ -20,7 +24,7 @@ Here is an example `change` method: change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, */ diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index edd01f17a97..e1bebe0fe5b 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,24 +1,23 @@ /* global Vue, VueResource, gl */ -/*= require vue_common_component/commit */ -/*= require vue_pagination/index */ -/*= require vue-resource -/*= require boards/vue_resource_interceptor */ -/*= require ./status.js.es6 */ -/*= require ./store.js.es6 */ -/*= require ./pipeline_url.js.es6 */ -/*= require ./stage.js.es6 */ -/*= require ./stages.js.es6 */ -/*= require ./pipeline_actions.js.es6 */ -/*= require ./time_ago.js.es6 */ -/*= require ./pipelines.js.es6 */ +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../vue_common_component/commit'); +require('../vue_pagination/index'); +require('../boards/vue_resource_interceptor'); +require('./status'); +require('./store'); +require('./pipeline_url'); +require('./stage'); +require('./stages'); +require('./pipeline_actions'); +require('./time_ago'); +require('./pipelines'); (() => { const project = document.querySelector('.pipelines'); const entry = document.querySelector('.vue-pipelines-index'); const svgs = document.querySelector('.pipeline-svgs'); - Vue.use(VueResource); - if (!entry) return null; return new Vue({ el: entry, 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 b195b0ef3ba..01f8b6519a4 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -26,10 +26,9 @@ v-if='actions' class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" data-toggle="dropdown" - title="Manual build" + title="Manual job" data-placement="top" - data-toggle="dropdown" - aria-label="Manual build" + aria-label="Manual job" > <span v-html='svgs.iconPlay' aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> @@ -54,7 +53,6 @@ data-toggle="dropdown" title="Artifacts" data-placement="top" - data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index b2ed05503c9..194bbae07d9 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,4 +1,4 @@ -/* global Vue, Turbolinks, gl */ +/* global Vue, gl */ /* eslint-disable no-param-reassign */ ((gl) => { @@ -36,7 +36,7 @@ }, methods: { change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, author(pipeline) { if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 9e19b1564dc..0f5ce2a9274 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -1,6 +1,6 @@ /* global gl, Flash */ /* eslint-disable no-param-reassign, no-underscore-dangle */ -/*= require vue_realtime_listener/index.js */ +require('../vue_realtime_listener'); ((gl) => { const pageValues = (headers) => { diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 index 23cac1466d2..95564152cce 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js.es6 +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -7,12 +7,12 @@ window.removeEventListener('beforeunload', removeIntervals); window.removeEventListener('focus', startIntervals); window.removeEventListener('blur', removeIntervals); - document.removeEventListener('page:fetch', removeAll); + document.removeEventListener('beforeunload', removeAll); }; window.addEventListener('beforeunload', removeIntervals); window.addEventListener('focus', startIntervals); window.addEventListener('blur', removeIntervals); - document.addEventListener('page:fetch', removeAll); + document.addEventListener('beforeunload', removeAll); }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ecff5fd5bf4..ef99b2e92f0 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -/*= require latinise */ -/*= require breakpoints */ -/*= require jquery.nicescroll */ +require('vendor/latinise'); +require('./breakpoints'); +require('vendor/jquery.nicescroll'); ((global) => { const dasherize = str => str.replace(/[_\s]+/g, '-'); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index a8b7be7ad06..d9261cda1b1 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -6,11 +6,11 @@ // /*= provides zen_mode:enter */ /*= provides zen_mode:leave */ -// -/*= require jquery.scrollTo */ -/*= require dropzone */ -/*= require mousetrap */ -/*= require mousetrap/pause */ + +require('vendor/jquery.scrollTo'); +window.Dropzone = require('dropzone'); +require('mousetrap'); +require('mousetrap/plugins/pause/mousetrap-pause'); // // ### Events diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 3cf49f4ff1b..08f203a1bf6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -31,7 +31,6 @@ @import "framework/modal.scss"; @import "framework/nav.scss"; @import "framework/pagination.scss"; -@import "framework/progress.scss"; @import "framework/panels.scss"; @import "framework/selects.scss"; @import "framework/sidebar.scss"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index bb6129158d9..cda46223492 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -330,10 +330,6 @@ } } -.btn-file-option { - background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); -} - .btn-build { margin-left: 10px; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6bfb9a6d1cb..ca5861bf3e6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -227,6 +227,11 @@ } } +.dropdown-menu-drop-up { + top: auto; + bottom: 100%; +} + .dropdown-menu-large { width: 340px; } diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss deleted file mode 100644 index e9800bd24b5..00000000000 --- a/app/assets/stylesheets/framework/progress.scss +++ /dev/null @@ -1,5 +0,0 @@ -html.turbolinks-progress-bar::before { - background-color: $progress-color!important; - height: 2px!important; - box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color; -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f2d60bff2b5..9b413f3e61c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -250,7 +250,7 @@ } .issue-boards-search { - width: 290px; + width: 395px; .form-control { display: inline-block; @@ -354,3 +354,135 @@ padding-right: 0; } } + +.add-issues-modal { + display: -webkit-flex; + display: flex; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba($black, .3); + z-index: 9999; +} + +.add-issues-container { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + width: 90vw; + height: 85vh; + max-width: 1100px; + min-height: 500px; + margin: auto; + padding: 25px 15px 0; + background-color: $white-light; + border-radius: $border-radius-default; + box-shadow: 0 2px 12px rgba($black, .5); + + .empty-state { + display: -webkit-flex; + display: flex; + -webkit-flex: 1; + flex: 1; + margin-top: 0; + + > .row { + width: 100%; + margin: auto 0; + } + + .svg-content { + margin-top: -40px; + } + } +} + +.add-issues-header { + margin: -25px -15px -5px; + border-top: 0; + border-bottom: 1px solid $border-color; + border-top-right-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; + + > h2 { + margin: 0; + font-size: 18px; + } +} + +.add-issues-search { + display: -webkit-flex; + display: flex; +} + +.add-issues-list-column { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 50%; + } + + @media (min-width: $screen-md-min) { + width: (100% / 3); + } +} + +.add-issues-list { + display: -webkit-flex; + display: flex; + -webkit-flex: 1; + flex: 1; + padding-top: 3px; + margin-left: -$gl-vert-padding; + margin-right: -$gl-vert-padding; + overflow-y: scroll; + + .card-parent { + padding: 0 5px 5px; + } + + .card { + border: 1px solid $border-gray-dark; + box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3); + cursor: pointer; + } +} + +.add-issues-list-loading { + -webkit-align-self: center; + align-self: center; + width: 100%; + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + font-size: 35px; +} + +.add-issues-footer { + margin: auto -15px 0; + padding-left: 15px; + padding-right: 15px; + border-bottom-right-radius: $border-radius-default; + border-bottom-left-radius: $border-radius-default; +} + +.add-issues-footer-to-list { + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + line-height: 34px; +} + +.issue-card-selected { + position: absolute; + right: -3px; + top: -3px; + width: 17px; + background-color: $blue-light; + color: $white-light; + border: 1px solid $border-blue-light; + font-size: 9px; + line-height: 15px; + border-radius: 50%; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ab68b360f93..0c013915a63 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -56,15 +56,24 @@ &.right { float: right; padding-right: 0; + } - a { - color: $gl-text-color; - } + .modify-merge-commit-link { + color: $gl-text-color; } - .remove_source_checkbox { + .merge-param-checkbox { margin: 0; } + + a .fa-question-circle { + color: $gl-text-color-secondary; + + &:hover, + &:focus { + color: $link-hover-color; + } + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index da0caa30c26..f310cc72da0 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -467,7 +467,7 @@ ul.notes { } .add-diff-note { - margin-top: -4px; + margin-top: -8px; border-radius: 40px; background: $white-light; padding: 4px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cf79c2e36c2..367a468e1ba 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -201,7 +201,8 @@ .stage-container { display: inline-block; position: relative; - margin-right: 6px; + height: 22px; + margin: 3px 6px 3px 0; .tooltip { white-space: nowrap; diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index b09ae423096..39c8c6d8a0c 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -45,7 +45,7 @@ class Admin::ProjectsController < Admin::ApplicationController protected def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:id]].join('') ) @project || render_404 diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index bc65dcc33d3..70ac6a75434 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController private def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:project_id]].join('') ) @project || render_404 diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 6f43ce5226d..6286d67d30c 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,13 +4,15 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables + start_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( - source_project: @project, - source_branch: @ref, - target_branch: @target_branch + start_project: @mr_target_project, + start_branch: start_branch, + target_branch: @mr_source_branch ) - result = service.new(@tree_edit_project, current_user, commit_params).execute + result = service.new( + @mr_source_project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) @@ -89,20 +91,18 @@ module CreatesCommit @mr_source_project != @mr_target_project end - def different_branch? - @mr_source_branch != @mr_target_branch || different_project? - end - def create_merge_request? - params[:create_merge_request].present? && different_branch? + # XXX: Even if the field is set, if we're checking the same branch + # as the target branch in the same project, + # we don't want to create a merge request. + params[:create_merge_request].present? && + (different_project? || @ref != @target_branch) end + # TODO: We should really clean this up def set_commit_variables - @mr_source_branch ||= @target_branch - if can?(current_user, :push_code, @project) # Edit file in this project - @tree_edit_project = @project @mr_source_project = @project if @project.forked? @@ -112,15 +112,34 @@ module CreatesCommit else # Merge request to this project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end else - # Edit file in fork - @tree_edit_project = current_user.fork_of(@project) # Merge request from fork to this project - @mr_source_project = @tree_edit_project + @mr_source_project = current_user.fork_of(@project) @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end + + @mr_source_branch = guess_mr_source_branch + end + + def initial_commit? + @mr_target_branch.nil? || + !@mr_target_project.repository.branch_exists?(@mr_target_branch) + end + + def guess_mr_source_branch + # XXX: Happens when viewing a commit without a branch. In this case, + # @target_branch would be the default branch for @mr_source_project, + # however we want a generated new branch here. Thus we can't use + # @target_branch, but should pass nil to indicate that we want a new + # branch instead of @target_branch. + return if + create_merge_request? && + # XXX: Don't understand why rubocop prefers this indention + @mr_source_project.repository.branch_exists?(@target_branch) + + @target_branch end end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 99acd98ae13..562f92bd83c 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index c08eb811532..3ba8c2f8bb9 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) - @last_push = current_user.recent_push - respond_to do |format| - format.html + format.html { @last_push = current_user.recent_push } format.atom do event_filter load_events diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f81237db991..264b14713fb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - @group.reset_path! + @group.restore_path! render action: "edit" end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b2ff36f6538..db33b60b229 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -18,13 +18,13 @@ class Projects::ApplicationController < ApplicationController # to # localhost/group/project # - if id =~ /\.git\Z/ + if params[:format] == 'git' redirect_to request.original_url.gsub(/\.git\/?\Z/, '') return end project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) + @project = Project.find_by_full_path(project_path) if can?(current_user, :read_project, @project) && !@project.pending_delete? if @project.path_with_namespace != project_path diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index dc33e1405f2..61fef4dc133 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -7,7 +7,7 @@ module Projects def index issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute - issues = issues.page(params[:page]) + issues = issues.page(params[:page]).per(params[:per] || 20) render json: { issues: serialize_as_json(issues), @@ -59,7 +59,7 @@ module Projects end def filter_params - params.merge(board_id: params[:board_id], id: params[:list_id]) + params.merge(board_id: params[:board_id], id: params[:list_id]).compact end def move_params @@ -73,7 +73,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index c871043efbd..b5a7078a3a1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -50,7 +50,7 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_change_commit_vars(@commit.revert_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -59,7 +59,7 @@ class Projects::CommitController < Projects::ApplicationController end def cherry_pick - assign_change_commit_vars(@commit.cherry_pick_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -116,11 +116,9 @@ class Projects::CommitController < Projects::ApplicationController } end - def assign_change_commit_vars(mr_source_branch) + def assign_change_commit_vars @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = mr_source_branch - @mr_target_branch = @target_branch @commit_params = { commit: @commit, create_merge_request: params[:create_merge_request].present? || different_project? diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d32966645c8..321cde255c3 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -46,7 +46,8 @@ class Projects::CompareController < Projects::ApplicationController end def define_diff_vars - @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) + @compare = CompareService.new(@project, @head_ref) + .execute(@project, @start_ref) if @compare @commits = @compare.commits diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 70845617d3c..216c158e41e 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -79,7 +79,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController if project_id.blank? @project = nil else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3492502e296..6eb542e4bd8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController title: merge_request.title, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, - coverage: coverage + coverage: coverage, + pipeline: pipeline.try(:id) } render json: response diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 02a97c1c574..5d193f26a8e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,8 +1,9 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = CreateSnippetService.new(@project, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(@project, current_user, create_params).execute if @snippet.valid? respond_with(@snippet, @@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index e617be8f9fb..50ba33ed570 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController namespace = params[:namespace_id] id = params[:project_id] - file_project = Project.find_with_namespace("#{namespace}/#{id}") + file_project = Project.find_by_full_path("#{namespace}/#{id}") if file_project.nil? @uploader = nil diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 444ff837bb3..acca821782c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -231,12 +231,16 @@ class ProjectsController < Projects::ApplicationController end def refs + branches = BranchesFinder.new(@repository, params).execute.map(&:name) + options = { - 'Branches' => @repository.branch_names, + 'Branches' => branches.take(100), } unless @repository.tag_count.zero? - options['Tags'] = VersionSorter.rsort(@repository.tag_names) + tags = TagsFinder.new(@repository, params).execute.map(&:name) + + options['Tags'] = tags.take(100) end # If reference is commit id - we should add it to branch/tag selectbox diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index dee57e4a388..b169d993688 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,5 +1,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -40,8 +41,8 @@ class SnippetsController < ApplicationController end def create - @snippet = CreateSnippetService.new(nil, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(nil, current_user, create_params).execute respond_with @snippet.becomes(Snippet) end @@ -96,6 +97,7 @@ class SnippetsController < ApplicationController end end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a112928c6de..bee323993a0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,7 +37,7 @@ module ApplicationHelper if project_id.is_a?(Project) project_id else - Project.find_with_namespace(project_id) + Project.find_by_full_path(project_id) end if project.avatar_url diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index c3508443d8a..311a70725ab 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -21,7 +21,7 @@ module BlobHelper options[:link_opts]) if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } + button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) link_to "Edit", edit_path, class: 'btn btn-sm' elsif can?(current_user, :fork_project, project) @@ -32,7 +32,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 38c586ccd31..f43827da446 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -6,7 +6,9 @@ module BoardsHelper endpoint: namespace_project_boards_path(@project.namespace, @project), board_id: board.id, disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: namespace_project_issues_path(@project.namespace, @project) + issue_link_base: namespace_project_issues_path(@project.namespace, @project), + root_path: root_path, + bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), } end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e9461b9f859..6dcb624c4da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -198,7 +198,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), - class: 'btn view-file js-view-file btn-file-option' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 0e456214d37..320dd89c9d3 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,5 +1,8 @@ module JavascriptHelper def page_specific_javascript_tag(js) - javascript_include_tag asset_path(js), { "data-turbolinks-track" => true } + javascript_include_tag asset_path(js) + end + def page_specific_javascript_bundle_tag(js) + javascript_include_tag(*webpack_asset_paths(js)) end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8c2c4e8833b..83ff898e68a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -143,4 +143,16 @@ module MergeRequestsHelper def different_base?(version1, version2) version1 && version2 && version1.base_commit_sha != version2.base_commit_sha end + + def merge_params(merge_request) + { + merge_when_build_succeeds: true, + should_remove_source_branch: true, + sha: merge_request.diff_head_sha + }.merge(merge_params_ee(merge_request)) + end + + def merge_params_ee(merge_request) + {} + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 37b69423c97..8ff8db16514 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -56,7 +56,7 @@ module SearchHelper { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") }, { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") }, - { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") }, { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }, ] end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 3a83ae15dd8..fc93acfe63e 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -93,10 +93,6 @@ module VisibilityLevelHelper current_application_settings.default_project_visibility end - def default_snippet_visibility - current_application_settings.default_snippet_visibility - end - def default_group_visibility current_application_settings.default_group_visibility end diff --git a/app/models/board.rb b/app/models/board.rb index c56422914a9..2780acc67c0 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,10 +5,6 @@ class Board < ActiveRecord::Base validates :project, presence: true - def backlog_list - lists.merge(List.backlog).take - end - def done_list lists.merge(List.done).take end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5fe8ddf69d7..b1f77bf242c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,29 +275,23 @@ module Ci end def update_coverage - return unless project - coverage_regex = project.build_coverage_regex - return unless coverage_regex coverage = extract_coverage(trace, coverage_regex) - - if coverage.is_a? Numeric - update_attributes(coverage: coverage) - end + update_attributes(coverage: coverage) if coverage.present? end def extract_coverage(text, regex) - begin - matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.kind_of?(Array) - coverage = matches.gsub(/\d+(\.\d+)?/).first + return unless regex - if coverage.present? - coverage.to_f - end - rescue - # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f end + rescue + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now end def has_trace_file? @@ -522,6 +516,10 @@ module Ci self.update(artifacts_expire_at: nil) end + def coverage_regex + super || project.try(:build_coverage_regex) + end + def when read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 1aa97debe42..1acff093aa1 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -34,7 +34,13 @@ module Spammable end def check_for_spam - self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + if spam? + self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") + end + end + + def spammable_entity_type + self.class.name.underscore end def spam_title diff --git a/app/models/list.rb b/app/models/list.rb index 065d75bd1dc..1e5da7f4dd4 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { backlog: 0, label: 1, done: 2 } + enum list_type: { label: 1, done: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index dadb81f9b6e..70bad2a4396 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. - CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) + CompareService.new(project, head_commit_sha) + .execute(project, sha, straight: straight) end def commits_count diff --git a/app/models/project.rb b/app/models/project.rb index 37f4705adbd..0d286bfbaa8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -370,10 +370,6 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end - - # Add alias for Routable method for compatibility with old code. - # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` - alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -1352,6 +1348,6 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) end end diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 25b5d777641..9bb456eee24 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -9,4 +9,8 @@ class ProjectSnippet < Snippet participant :author participant :notes_with_associations + + def check_for_spam? + super && project.public? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index d77b7692d75..7cf09c52bf4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -5,7 +5,7 @@ class Repository attr_accessor :path_with_namespace, :project - class CommitError < StandardError; end + CommitError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -64,10 +64,6 @@ class Repository @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) end - def update_autocrlf_option - raw_repository.autocrlf = :input if raw_repository.autocrlf != :input - end - # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -168,63 +164,46 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = commit(target).try(:id) + def add_branch(user, branch_name, ref) + newrev = commit(ref).try(:sha) - return false unless target + return false unless newrev - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - update_ref!(ref, target, oldrev) - end + GitOperationService.new(user, self).add_branch(branch_name, newrev) after_create_branch find_branch(branch_name) end def add_tag(user, tag_name, target, message = nil) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - target = commit(target).try(:id) - - return false unless target - + newrev = commit(target).try(:id) options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| - raw_tag = rugged.tags.create(tag_name, target, options) - service.newrev = raw_tag.target_id - end + return false unless newrev + + GitOperationService.new(user, self).add_tag(tag_name, newrev, options) find_tag(tag_name) end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - oldrev = branch.try(:dereferenced_target).try(:id) - newrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - end + GitOperationService.new(user, self).rm_branch(branch) after_remove_branch true end - def rm_tag(tag_name) + def rm_tag(user, tag_name) before_remove_tag + tag = find_tag(tag_name) - begin - rugged.tags.delete(tag_name) - true - rescue Rugged::ReferenceError - false - end + GitOperationService.new(user, self).rm_tag(tag) + + after_remove_tag + true end def ref_names @@ -241,21 +220,6 @@ class Repository false end - def update_ref!(name, newrev, oldrev) - # We use 'git update-ref' because libgit2/rugged currently does not - # offer 'compare and swap' ref updates. Without compare-and-swap we can - # (and have!) accidentally reset the ref to an earlier state, clobbering - # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) - _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| - stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") - end - - return if status.zero? - - raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") - end - # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -435,6 +399,11 @@ class Repository repository_event(:remove_tag) end + # Runs code after removing a tag. + def after_remove_tag + expire_tags_cache + end + def before_import expire_content_cache end @@ -779,121 +748,132 @@ class Repository @tags ||= raw_repository.tags end - def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - raw_repository.mkdir(path, options) - end - end - - def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: update - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) + # 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) - Gitlab::Git::Blob.commit(raw_repository, options) + if start_branch_name + start_project.repository. + check_tree_entry_for_dir(start_branch_name, path) end - end - - def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: true - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - if previous_path && previous_path != path - options[:file][:previous_path] = previous_path - Gitlab::Git::Blob.rename(raw_repository, options) - else - Gitlab::Git::Blob.commit(raw_repository, options) + 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) + 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" + + if tree_entry_at(branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end - end - end - - def remove_file(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - path: path - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - Gitlab::Git::Blob.remove(raw_repository, options) + if start_branch_name && + start_project.repository.tree_entry_at(start_branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) + end end - end - def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + 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 + + # 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 }]) + 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 }]) + 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 = [] - branch = find_branch(ref) - if branch - last_commit = branch.dereferenced_target - index.read_tree(last_commit.raw_commit.tree) - parents = [last_commit.sha] - end + parents = if start_commit + index.read_tree(start_commit.raw_commit.tree) + [start_commit.sha] + else + [] + end - actions.each do |action| - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(action[:file_path])[:mode] - when :move - index.get(action[:previous_path])[:mode] - end - mode ||= 0o100644 - - index.remove(action[:previous_path]) if action[:action] == :move - - content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] - oid = rugged.write(content, :blob) - - index.add(path: action[:file_path], oid: oid, mode: mode) - when :delete - index.remove(action[:file_path]) - end + actions.each do |act| + git_action(index, act) end options = { @@ -906,6 +886,7 @@ class Repository Rugged::Commit.create(rugged, options) end end + # rubocop:enable Metrics/ParameterLists def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) @@ -918,7 +899,7 @@ class Repository end def user_to_committer(user) - Gitlab::Git::committer_hash(email: user.email, name: user.name) + Gitlab::Git.committer_hash(email: user.email, name: user.name) end def can_be_merged?(source_sha, target_branch) @@ -932,17 +913,18 @@ class Repository end end - def merge(user, merge_request, options = {}) - our_commit = rugged.branches[merge_request.target_branch].target - their_commit = rugged.lookup(merge_request.diff_head_sha) + def merge(user, source, merge_request, options = {}) + GitOperationService.new(user, self).with_branch( + merge_request.target_branch) do |start_commit| + our_commit = start_commit.sha + their_commit = source - raise "Invalid merge target" if our_commit.nil? - raise "Invalid merge source" if their_commit.nil? + raise 'Invalid merge target' unless our_commit + raise 'Invalid merge source' unless their_commit - merge_index = rugged.merge_commits(our_commit, their_commit) - return false if merge_index.conflicts? + merge_index = rugged.merge_commits(our_commit, their_commit) + break if merge_index.conflicts? - update_branch_with_hooks(user, merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -952,34 +934,48 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end + rescue Repository::CommitError # when merge_index.conflicts? + false end - def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha - revert_tree_id ||= check_revert_content(commit, base_branch) + def revert( + user, commit, branch_name, revert_tree_id = nil, + start_branch_name: nil, start_project: project) + revert_tree_id ||= check_revert_content(commit, branch_name) return false unless revert_tree_id - update_branch_with_hooks(user, base_branch) do + GitOperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| + committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + + Rugged::Commit.create(rugged, message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [start_commit.sha]) end end - def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha - cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + def cherry_pick( + user, commit, branch_name, cherry_pick_tree_id = nil, + start_branch_name: nil, start_project: project) + cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) return false unless cherry_pick_tree_id - update_branch_with_hooks(user, base_branch) do + GitOperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| + committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + + Rugged::Commit.create(rugged, message: commit.message, author: { email: commit.author_email, @@ -988,22 +984,22 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [start_commit.sha]) end end - def resolve_conflicts(user, branch, params) - update_branch_with_hooks(user, branch) do + def resolve_conflicts(user, branch_name, params) + GitOperationService.new(user, self).with_branch(branch_name) do committer = user_to_committer(user) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) end end - def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha - args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + def check_revert_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << { mainline: 1 } if target_commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -1014,10 +1010,10 @@ class Repository tree_id end - def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha - args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + def check_cherry_pick_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << 1 if target_commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? @@ -1075,6 +1071,28 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end + def with_repo_branch_commit(start_repository, start_branch_name) + branch_name_or_sha = + if start_repository == self + start_branch_name + else + tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" + + fetch_ref( + start_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + tmp_ref + ) + + start_repository.commit(start_branch_name).sha + end + + yield(commit(branch_name_or_sha)) + + ensure + rugged.references.delete(tmp_ref) if tmp_ref + end + def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) @@ -1084,39 +1102,6 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - def update_branch_with_hooks(current_user, branch) - update_autocrlf_option - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - target_branch = find_branch(branch) - was_empty = empty? - - # Make commit - newrev = yield(ref) - - unless newrev - raise CommitError.new('Failed to create commit') - end - - if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? - oldrev = Gitlab::Git::BLANK_SHA - else - oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) - end - - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - - if was_empty || !target_branch - # If repo was empty expire cache - after_create if was_empty - after_create_branch - end - end - - newrev - end - def ls_files(ref) actual_ref = ref || root_ref raw_repository.ls_files(actual_ref) @@ -1175,8 +1160,76 @@ class Repository end 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 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 + + 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/snippet.rb b/app/models/snippet.rb index 771a7350556..2665a7249a3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base include Sortable include Awardable include Mentionable + include Spammable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content @@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for :visibility_level, Snippet::PRIVATE + default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project @@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base participant :author participant :notes_with_associations + attr_spammable :title, spam_title: true + attr_spammable :content, spam_description: true + def self.reference_prefix '$' end @@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base notes.includes(:author) end + def check_for_spam? + public? + end + + def spammable_entity_type + 'snippet' + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c00c5aebf57..5cb7a86a5ee 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -61,7 +61,7 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) + requested_project = Project.find_by_full_path(name) return unless requested_project actions = actions.select do |action| diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 9bdd7b6f0cf..f6275a63109 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,7 +12,6 @@ module Boards def create_board! board = project.boards.create - board.lists.create(list_type: :backlog) board.lists.create(list_type: :done) board diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index fd4a462c7b2..8a94c54b6ab 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,8 +3,8 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless list.movable? - issues = with_list_label(issues) if list.movable? + issues = without_board_labels(issues) unless movable_list? + issues = with_list_label(issues) if movable_list? issues end @@ -15,7 +15,13 @@ module Boards end def list - @list ||= board.lists.find(params[:id]) + return @list if defined?(@list) + + @list = board.lists.find(params[:id]) if params.key?(:id) + end + + def movable_list? + @movable_list ||= list.present? && list.movable? end def filter_params @@ -40,7 +46,7 @@ module Boards end def set_state - params[:state] = list.done? ? 'closed' : 'opened' + params[:state] = list && list.done? ? 'closed' : 'opened' end def board_label_ids diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 4d410f66c55..25e22f14e60 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -4,7 +4,8 @@ module Commits class ChangeError < StandardError; end def execute - @source_project = params[:source_project] || @project + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit = params[:commit] @create_merge_request = params[:create_merge_request].present? @@ -25,13 +26,28 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch - tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch) + if @create_merge_request + into = @commit.public_send("#{action}_branch_name") + tree_branch = @start_branch + else + into = tree_branch = @target_branch + end + + tree_id = repository.public_send( + "check_#{action}_content", @commit, tree_branch) if tree_id - create_target_branch(into) if @create_merge_request + validate_target_branch(into) if @create_merge_request + + repository.public_send( + action, + current_user, + @commit, + into, + tree_id, + start_project: @start_project, + start_branch_name: @start_branch) - repository.public_send(action, current_user, @commit, into, tree_id) success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. @@ -50,12 +66,12 @@ module Commits true end - def create_target_branch(new_branch) + def validate_target_branch(new_branch) # Temporary branch exists and contains the change commit - return success if repository.find_branch(new_branch) + return if repository.find_branch(new_branch) - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) + result = ValidateNewBranchService.new(@project, current_user) + .execute(new_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 5e8fafca98c..ab4c02a97a0 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,23 +3,27 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch, straight: false) - source_commit = source_project.commit(source_branch) - return unless source_commit + attr_reader :start_project, :start_branch_name - source_sha = source_commit.sha + def initialize(new_start_project, new_start_branch_name) + @start_project = new_start_project + @start_branch_name = new_start_branch_name + end + def execute(target_project, target_branch, straight: false) # If compare with other project we need to fetch ref first - unless target_project == source_project - random_string = SecureRandom.hex + target_project.repository.with_repo_branch_commit( + start_project.repository, + start_branch_name) do |commit| + break unless commit - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - "refs/tmp/#{random_string}/head" - ) + compare(commit.sha, target_project, target_branch, straight) end + end + + private + def compare(source_sha, target_project, target_branch, straight) raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index e004a303496..77459d8779d 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,31 +1,11 @@ class CreateBranchService < BaseService - def execute(branch_name, ref, source_project: @project) - valid_branch = Gitlab::GitRefValidator.validate(branch_name) + def execute(branch_name, ref) + result = ValidateNewBranchService.new(project, current_user) + .execute(branch_name) - unless valid_branch - return error('Branch name is invalid') - end - - repository = project.repository - existing_branch = repository.find_branch(branch_name) - - if existing_branch - return error('Branch already exists') - end - - new_branch = if source_project != @project - repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{ref}", - "refs/heads/#{branch_name}" - ) - - repository.after_create_branch + return result if result[:status] == :error - repository.find_branch(branch_name) - else - repository.add_branch(current_user, branch_name, ref) - end + new_branch = repository.add_branch(current_user, branch_name, ref) if new_branch success(new_branch) diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 95cc9baf406..14f5ba064ff 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -1,5 +1,8 @@ class CreateSnippetService < BaseService def execute + request = params.delete(:request) + api = params.delete(:api) + snippet = if project project.snippets.build(params) else @@ -12,8 +15,12 @@ class CreateSnippetService < BaseService end snippet.author = current_user + snippet.spam = SpamService.new(snippet, request).check(api) + + if snippet.save + UserAgentDetailService.new(snippet, request).create + end - snippet.save snippet end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index a44dee14a0f..9d4bffb93e9 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -7,7 +7,7 @@ class DeleteTagService < BaseService return error('No such tag', 404) end - if repository.rm_tag(tag_name) + if repository.rm_tag(current_user, tag_name) release = project.releases.find_by(tag: tag_name) release.destroy if release diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 9bd4bd464f7..0a25f56d24c 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,9 +3,9 @@ module Files class ValidationError < StandardError; end def execute - @source_project = params[:source_project] || @project - @source_branch = params[:source_branch] - @target_branch = params[:target_branch] + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] + @target_branch = params[:target_branch] @commit_message = params[:commit_message] @file_path = params[:file_path] @@ -22,10 +22,8 @@ module Files # Validate parameters validate - # Create new branch if it different from source_branch - if different_branch? - create_target_branch - end + # Create new branch if it different from start_branch + validate_target_branch if different_branch? result = commit if result @@ -40,7 +38,7 @@ module Files private def different_branch? - @source_branch != @target_branch || @source_project != @project + @start_branch != @target_branch || @start_project != @project end def file_has_changed? @@ -61,22 +59,23 @@ module Files end unless project.empty_repo? - unless @source_project.repository.branch_names.include?(@source_branch) + unless @start_project.repository.branch_exists?(@start_branch) raise_error('You can only create or edit files when you are on a branch') end if different_branch? - if repository.branch_names.include?(@target_branch) + if repository.branch_exists?(@target_branch) raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') end end end end - def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) + def validate_target_branch + result = ValidateNewBranchService.new(project, current_user). + execute(@target_branch) - unless result[:status] == :success + if result[:status] == :error raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index e5b4d60e467..858de5f0538 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,7 +1,15 @@ module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.commit_dir( + current_user, + @file_path, + message: @commit_message, + branch_name: @target_branch, + author_email: @author_email, + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index b23576b9a28..88dd7bbaedb 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,7 +1,17 @@ module Files class CreateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name) + repository.commit_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, + start_branch_name: @start_branch) end def validate @@ -24,7 +34,7 @@ module Files unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@source_branch, @file_path) + blob = repository.blob_at_branch(@start_branch, @file_path) if blob raise_error('Your changes could not be committed because a file with the same name already exists') diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 4f7e7a5baaa..50f0ffcac9f 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,7 +1,15 @@ module Files class DeleteService < Files::BaseService def commit - repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.remove_file( + current_user, + @file_path, + message: @commit_message, + branch_name: @target_branch, + author_email: @author_email, + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 54446e90007..6ba868df04d 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -5,11 +5,13 @@ module Files def commit repository.multi_action( user: current_user, - branch: @target_branch, message: @commit_message, + branch_name: @target_branch, actions: params[:actions], author_email: @author_email, - author_name: @author_name + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch ) end @@ -61,7 +63,7 @@ module Files end def last_commit - Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path) + Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path) end def regex_check(file) diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 47a18e3e132..a71fe61a4b6 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -4,11 +4,13 @@ module Files def commit repository.update_file(current_user, @file_path, @file_content, - branch: @target_branch, - previous_path: @previous_path, message: @commit_message, + branch_name: @target_branch, + previous_path: @previous_path, author_email: @author_email, - author_name: @author_name) + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end private @@ -23,7 +25,7 @@ module Files def last_commit @last_commit ||= Gitlab::Git::Commit. - last_for_path(@source_project.repository, @source_branch, @file_path) + last_for_path(@start_project.repository, @start_branch, @file_path) end end end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 6cd3908d43a..d222d1e63aa 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -18,9 +18,9 @@ class GitHooksService end end - yield self - - run_hook('post-receive') + yield(self).tap do + run_hook('post-receive') + end end private diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb new file mode 100644 index 00000000000..27bcc047601 --- /dev/null +++ b/app/services/git_operation_service.rb @@ -0,0 +1,179 @@ +class GitOperationService + attr_reader :user, :repository + + def initialize(new_user, new_repository) + @user = new_user + @repository = new_repository + end + + def add_branch(branch_name, newrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def rm_branch(branch) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name + oldrev = branch.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def add_tag(tag_name, newrev, options = {}) + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks(ref, newrev, oldrev) do |service| + # We want to pass the OID of the tag object to the hooks. For an + # annotated tag we don't know that OID until after the tag object + # (raw_tag) is created in the repository. That is why we have to + # update the value after creating the tag object. Only the + # "post-receive" hook will receive the correct value in this case. + raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + service.newrev = raw_tag.target_id + end + end + + def rm_tag(tag) + ref = Gitlab::Git::TAG_REF_PREFIX + tag.name + oldrev = tag.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) do + repository.rugged.tags.delete(tag_name) + end + end + + # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `start_branch_name`. + # If `start_project` is passed, and the branch doesn't exist, + # it would try to find the commits from it instead of current repository. + def with_branch( + branch_name, + start_branch_name: nil, + start_project: repository.project, + &block) + + check_with_branch_arguments!( + branch_name, start_branch_name, start_project) + + update_branch_with_hooks(branch_name) do + repository.with_repo_branch_commit( + start_project.repository, + start_branch_name || branch_name, + &block) + end + end + + private + + def update_branch_with_hooks(branch_name) + update_autocrlf_option + + was_empty = repository.empty? + + # Make commit + newrev = yield + + unless newrev + raise Repository::CommitError.new('Failed to create commit') + end + + branch = repository.find_branch(branch_name) + oldrev = find_oldrev_from_branch(newrev, branch) + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + update_ref_in_hooks(ref, newrev, oldrev) + + # If repo was empty expire cache + repository.after_create if was_empty + repository.after_create_branch if + was_empty || Gitlab::Git.blank_ref?(oldrev) + + newrev + end + + def find_oldrev_from_branch(newrev, branch) + return Gitlab::Git::BLANK_SHA unless branch + + oldrev = branch.target + + if oldrev == repository.rugged.merge_base(newrev, branch.target) + oldrev + else + raise Repository::CommitError.new('Branch diverged') + end + end + + def update_ref_in_hooks(ref, newrev, oldrev) + with_hooks(ref, newrev, oldrev) do + update_ref(ref, newrev, oldrev) + end + end + + def with_hooks(ref, newrev, oldrev) + GitHooksService.new.execute( + user, + repository.path_to_repo, + oldrev, + newrev, + ref) do |service| + + yield(service) + end + end + + def update_ref(ref, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] + _, status = Gitlab::Popen.popen( + command, + repository.path_to_repo) do |stdin| + stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") + end + + unless status.zero? + raise Repository::CommitError.new( + "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ + " Please refresh and try again.") + end + end + + def update_autocrlf_option + if repository.raw_repository.autocrlf != :input + repository.raw_repository.autocrlf = :input + end + end + + def check_with_branch_arguments!( + branch_name, start_branch_name, start_project) + return if repository.branch_exists?(branch_name) + + if repository.project != start_project + unless start_branch_name + raise ArgumentError, + 'Should also pass :start_branch_name if' + + ' :start_project is different from current project' + end + + unless start_project.repository.branch_exists?(start_branch_name) + raise ArgumentError, + "Cannot find branch #{branch_name} nor" \ + " #{start_branch_name} from" \ + " #{start_project.path_with_namespace}" + end + elsif start_branch_name + unless repository.branch_exists?(start_branch_name) + raise ArgumentError, + "Cannot find branch #{branch_name} nor" \ + " #{start_branch_name} from" \ + " #{repository.project.path_with_namespace}" + end + end + end +end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 1d6d2754559..f4d52e3ebbd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -47,9 +47,10 @@ module MergeRequests end def compare_branches - compare = CompareService.new.execute( + compare = CompareService.new( source_project, - source_branch, + source_branch + ).execute( target_project, target_branch ) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index ab9056a3250..5ca6fec962d 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,13 +6,17 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request + attr_reader :merge_request, :source def execute(merge_request) @merge_request = merge_request return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? + @source = find_merge_source + + return log_merge_error('No source for merge', true) unless @source + merge_request.in_locked_state do if commit after_merge @@ -34,7 +38,7 @@ module MergeRequests committer: committer } - commit_id = repository.merge(current_user, merge_request, options) + commit_id = repository.merge(current_user, source, merge_request, options) if commit_id merge_request.update(merge_commit_sha: commit_id) @@ -73,9 +77,11 @@ module MergeRequests end def merge_request_info - project = merge_request.project + merge_request.to_reference(full: true) + end - "#{project.to_reference}#{merge_request.to_reference}" + def find_merge_source + merge_request.diff_head_sha end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 3566a8ba92f..3e0a85cf059 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -304,6 +304,18 @@ module SlashCommands params '@user' command :cc + desc 'Defines target branch for MR' + params '<Local branch name>' + condition do + issuable.respond_to?(:target_branch) && + (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || + issuable.new_record?) + end + command :target_branch do |target_branch_param| + branch_name = target_branch_param.strip + @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) + end + def find_label_ids(labels_param) label_ids_by_reference = extract_references(labels_param, :label).map(&:id) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb new file mode 100644 index 00000000000..2f61be184ce --- /dev/null +++ b/app/services/validate_new_branch_service.rb @@ -0,0 +1,22 @@ +require_relative 'base_service' + +class ValidateNewBranchService < BaseService + def execute(branch_name) + valid_branch = Gitlab::GitRefValidator.validate(branch_name) + + unless valid_branch + return error('Branch name is invalid') + end + + repository = project.repository + existing_branch = repository.find_branch(branch_name) + + if existing_branch + return error('Branch already exists') + end + + success + rescue GitHooksService::PreReceiveError => ex + error(ex.message) + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 558bbe07b16..e7701d75a6e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -204,7 +204,7 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each build's artifacts can have + Set the maximum file size each jobs's artifacts can have = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") - if Gitlab.config.registry.enabled diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 5e3f105d41f..66d633119c2 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -12,7 +12,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'all').capitalize} builds + #{(@scope || 'all').capitalize} jobs %ul.content-list.builds-content-list.admin-builds-table = render "projects/builds/table", builds: @builds, admin: true diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index b5f96363230..7893c1dee97 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -20,9 +20,9 @@ %span Groups = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do + = link_to admin_builds_path, title: 'Jobs' do %span - Builds + Jobs = nav_link path: ['runners#index', 'runners#show'] do = link_to admin_runners_path, title: 'Runners' do %span diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 124f970524e..721bc77cc2f 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -26,7 +26,7 @@ .bs-callout %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, even on your local machine. @@ -37,16 +37,16 @@ %ul %li %span.label.label-success shared - \- Runner runs builds from all unassigned projects + \- Runner runs jobs from all unassigned projects %li %span.label.label-info specific - \- Runner runs builds from assigned projects + \- Runner runs jobs from assigned projects %li %span.label.label-warning locked \- Runner cannot be assigned to other projects %li %span.label.label-danger paused - \- Runner will not receive any new builds + \- Runner will not receive any new jobs .append-bottom-20.clearfix .pull-left @@ -68,7 +68,7 @@ %th Runner token %th Description %th Projects - %th Builds + %th Jobs %th Tags %th Last contact %th diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 39e103e3062..dc4116e1ce0 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -11,13 +11,13 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This Runner will process builds from ALL UNASSIGNED projects + %h4 This Runner will process jobs from ALL UNASSIGNED projects %p If you want Runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This Runner will process builds only from ASSIGNED projects + %h4 This Runner will process jobs only from ASSIGNED projects %p You can't make this a shared Runner. %hr @@ -70,11 +70,11 @@ = paginate @projects, theme: "gitlab" .col-md-6 - %h4 Recent builds served by this Runner + %h4 Recent jobs served by this Runner %table.table.ci-table.runner-builds %thead %tr - %th Build + %th Job %th Status %th Project %th Commit diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index e87a16a5157..f92f89e73ff 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -6,4 +6,4 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn') diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index b185b81db7f..5b1a4630c56 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -3,7 +3,7 @@ .col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. + Search for members by name, username, or email, or invite new ones using their email address. .col-md-3.col-lg-2 = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" @@ -16,7 +16,7 @@ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this group and all of its projects. + On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f4c432a095a..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -7,7 +7,7 @@ - if can?(current_user, :admin_group_member, @group) .project-members-new.append-bottom-default %p.clearfix - Add new user to + Add new member to %strong= @group.name = render "new_group_member" @@ -15,7 +15,7 @@ .append-bottom-default.clearfix %h5.member.existing-title - Existing users + Existing members = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } @@ -24,7 +24,7 @@ = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading - Users with access to + Members with access to %strong= @group.name %span.badge= @members.total_count %ul.content-list diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index b74cc822295..da2df0d8080 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -143,7 +143,7 @@ .key g .key b %td - Go to builds + Go to jobs %tr %td.shortcut .key g diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 7f1b9ee7141..e18bd47798b 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -82,7 +82,7 @@ rather than Git. Please convert = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' + = link_to 'import flow', status_import_bitbucket_path again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3096f0ee19e..f2d355587bd 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,11 +28,13 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" - = javascript_include_tag "application" + = javascript_include_tag(*webpack_asset_paths("application")) - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts + = yield :project_javascripts + = csrf_meta_tags - unless browser.safari? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 935517d4913..248d439cd05 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,9 +4,6 @@ %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data - -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. - = yield :scripts_body_top - = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a8bbd67de80..7883823b21e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -96,8 +96,8 @@ -# Shortcut to builds page - if project_nav_tab? :builds %li.hidden - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - Builds + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs -# Shortcut to commits page - if project_nav_tab? :commits diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 277eb71ea73..f5e7ea7710d 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -3,7 +3,7 @@ - header_title project_title(@project) unless header_title - nav "project" -- content_for :scripts_body_top do +- content_for :project_javascripts do - project = @target_project || @project - if @project_wiki && @page - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index a744c4be9d6..060b50ffc69 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build failed) + GitLab (job failed) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 9d497983498..2a94688a6b0 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -1,4 +1,4 @@ -Build failed for <%= @project.name %> +Job failed for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 8c2e6db1426..ca0eaa96a9d 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build successful) + GitLab (job successful) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index c5ed4f84861..445cd46e64f 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -1,4 +1,4 @@ -Build successful for <%= @project.name %> +Job successful for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb index f495a2e5486..741c7f344c8 100644 --- a/app/views/notify/links/ci/builds/_build.text.erb +++ b/app/views/notify/links/ci/builds/_build.text.erb @@ -1 +1 @@ -Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) +Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb index 8e89c52a1f3..af8924bad57 100644 --- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb +++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb @@ -1 +1 @@ -Build #<%= build.id %> +Job #<%= build.id %> diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml index 943ebdaeffe..1df04ea614e 100644 --- a/app/views/profiles/_head.html.haml +++ b/app/views/profiles/_head.html.haml @@ -1,3 +1,3 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/cropper.js') - = page_specific_javascript_tag('profile/profile_bundle.js') + = page_specific_javascript_bundle_tag('profile') diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 14b330d16ad..a4f4079d556 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -82,7 +82,7 @@ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect - else - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do Connect %hr - if current_user.can_change_username? diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index e2b73cee5a9..a41791f0eca 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -3,6 +3,6 @@ %h4 Customize your workflow! %p - Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production! + Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production! - if can?(current_user, :admin_project, @project) = link_to "Get started", edit_project_path(@project), class: "btn btn-success" diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 1a1327fb53c..27d25a6b682 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -4,10 +4,10 @@ .checkbox.builds-feature = form.label :only_allow_merge_if_build_succeeds do = form.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds + %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr - Builds need to be configured to enable this feature. + Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds') .checkbox = form.label :only_allow_merge_if_all_discussions_are_resolved do diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index d0ff14e45e6..edf55d59f28 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,4 +1,4 @@ -- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' +- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 538f8591f13..3b1a2e54ec2 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -27,4 +27,4 @@ - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to, class: line_class } - = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true + = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a5dcd93f42e..8853801016b 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') + = page_specific_javascript_bundle_tag('blob_edit') = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index b6ed9518c48..e0ce8cc9601 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,7 @@ - page_title "New File", @path.presence, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') + = page_specific_javascript_bundle_tag('blob_edit') %h3.page-title New File diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 356bd50f7f3..f0c76af29dc 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -3,8 +3,8 @@ - page_title "Boards" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('boards/boards_bundle.js') - = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? + = page_specific_javascript_bundle_tag('boards') + = page_specific_javascript_bundle_tag('boards_test') if Rails.env.test? %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" @@ -24,5 +24,10 @@ ":list" => "list", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":key" => "_uid" } = render "projects/boards/components/sidebar" + %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), + "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index a2e5118a9f3..72bce4049de 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -29,6 +29,7 @@ ":loading" => "list.loading", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", "ref" => "board-list" } - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index 34fdb1f6a74..f413a5e94c1 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -34,6 +34,7 @@ ":list" => "list", ":issue" => "issue", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":disabled" => "disabled", ":key" => "issue.id" } %li.board-list-count.text-center{ "v-if" => "showCount" } diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e4c2aff46ec..891c2c46251 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -4,25 +4,7 @@ "@mousedown" => "mouseDown", "@mousemove" => "mouseMove", "@mouseup" => "showIssue($event)" } - %h4.card-title - = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") - %a{ ":href" => 'issueLinkBase + "/" + issue.id', - ":title" => "issue.title" } - {{ issue.title }} - .card-footer - %span.card-number{ "v-if" => "issue.id" } - = precede '#' do - {{ issue.id }} - %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username", - ":title" => '"Assigned to " + issue.assignee.name', - "v-if" => "issue.assignee", - data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" } - %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", - type: "button", - "v-if" => "(!list.label || label.id !== list.label.id)", - "@click" => "filterByLabel(label, $event)", - ":style" => "{ backgroundColor: label.color, color: label.textColor }", - ":title" => "label.description", - data: { container: 'body' } } - {{ label.title }} + %issue-card-inner{ ":list" => "list", + ":issue" => "issue", + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index df7fa9ddaf2..24d76da6f06 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -22,3 +22,5 @@ = render "projects/boards/components/sidebar/due_date" = render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/notifications" + %remove-btn{ ":issue" => "issue", + ":list" => "list" } diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 736b485bf06..27e81c2bec3 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,7 +1,7 @@ .content-block.build-header .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false - Build + Job %strong.js-build-id ##{@build.id} in pipeline = link_to pipeline_path(@build.pipeline) do @@ -17,6 +17,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', 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/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 37bf085130a..56fc5f5e68b 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -2,7 +2,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default - Build + Job %strong ##{@build.id} %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } = icon('angle-double-right') @@ -17,7 +17,7 @@ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } .title - Build artifacts + Job artifacts - if @build.artifacts_expired? %p.build-detail-row The artifacts were removed @@ -42,9 +42,9 @@ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .title - Build details + Job details - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: @@ -136,4 +136,4 @@ - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' } + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 028664f5bba..acfdb250aff 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -2,14 +2,14 @@ - if builds.blank? %div - .nothing-here-block No builds to show + .nothing-here-block No jobs to show - else .table-holder %table.table.ci-table.builds-page %thead %tr %th Status - %th Build + %th Job %th Pipeline - if admin %th Project diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index c623e39b21f..5ffc0e20d10 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Builds" +- page_title "Jobs" = render "projects/pipelines/head" %div{ class: container_class } @@ -14,7 +14,7 @@ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c613e473e4c..228dad528ab 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "#{@build.name} (##{@build.id})", "Builds" +- page_title "#{@build.name} (##{@build.id})", "Jobs" - trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true @@ -12,14 +12,14 @@ .bs-callout.bs-callout-warning %p - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have any runners online assigned to it. + This job is stuck, because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? - This build is stuck, because you don't have any active runners online with any of these tags assigned to them: + This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| %span.label.label-primary = tag - else - This build is stuck, because you don't have any active runners that can run this build. + This job is stuck, because you don't have any active runners that can run this job. %br Go to @@ -37,14 +37,14 @@ - environment = environment_for_build(@build.project, @build) - if @build.success? && @build.last_deployment.present? - if @build.last_deployment.last? - This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. + This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - else - This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. + This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. View the most recent deployment #{deployment_link(environment.last_deployment)}. - elsif @build.complete? && !@build.success? - The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed. + The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - else - This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} + This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} @@ -52,9 +52,9 @@ - if @build.erased? .erased.alert.alert-warning - if @build.erased_by_user? - Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - else - Build has been erased #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - else #js-build-scroll.scroll-controls .scroll-step diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index c1e496455d1..5ea85f9fd4c 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -32,10 +32,10 @@ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') - if retried - = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') .label-container - if build.tags.any? diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 818a70f38f1..cdab1e1b1a6 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -15,7 +15,7 @@ - else %span.api.monospace API - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered - if pipeline.yaml_errors.present? @@ -78,7 +78,7 @@ .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 build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } = custom_icon('icon_play') = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 08d3443b3d0..6abff6aaf95 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -13,7 +13,7 @@ Pipeline = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" with - = pluralize pipeline.statuses.count(:id), "build" + = pluralize pipeline.statuses.count(:id), "job" - if pipeline.ref for = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" @@ -44,7 +44,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 479ce44f378..5405ff16bea 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title "Cycle Analytics" - content_for :page_specific_javascripts do - = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js") + = page_specific_javascript_bundle_tag('cycle_analytics') = render "projects/pipelines/head" diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c37a33bbcd5..fc478ccc995 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -5,7 +5,7 @@ - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - if editable_diff?(diff_file) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index f361204ecac..074f1f634ae 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,15 +1,17 @@ / Side-by-side diff view .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - - last_line = 0 - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] - - last_line = right.new_pos if right %tr.line_holder.parallel - if left - - if left.meta? + - case left.type + - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel + - when 'nonewline' + %td.old_line.diff-line-num + %td.line_content.match= left.text - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) @@ -21,8 +23,12 @@ %td.line_content.parallel - if right - - if right.meta? + - case right.type + - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel + - when 'nonewline' + %td.new_line.diff-line-num + %td.line_content.match= right.text - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) @@ -37,5 +43,7 @@ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - if discussion_left || discussion_right = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - - if !diff_file.new_file && last_line > 0 - = diff_match_line last_line, last_line, bottom: true, view: :parallel + - if !diff_file.new_file && diff_file.diff_lines.any? + - last_line = diff_file.diff_lines.last + %tr.line_holder.parallel + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index f1d2d4bf268..2eea1db169a 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -4,13 +4,13 @@ %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - - last_line = 0 - discussions = @grouped_diff_discussions unless @diff_notes_disabled = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, discussions: discussions } - - last_line = diff_file.highlighted_diff_lines.last.new_pos - - if !diff_file.new_file && last_line > 0 - = diff_match_line last_line, last_line, bottom: true + - if !diff_file.new_file && diff_file.highlighted_diff_lines.any? + - last_line = diff_file.highlighted_diff_lines.last + %tr.line_holder + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ec944d4ffb7..7a2dacdb1e7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -63,7 +63,7 @@ .row .col-md-9.project-feature.nested - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' %span.help-block Submit, test and deploy your changes before merge .col-md-3 = project_feature_access_select(:builds_access_level) @@ -180,13 +180,13 @@ %p The following items will NOT be exported: %ul - %li Build traces and artifacts + %li Job traces and artifacts %li LFS objects %li Container registry images %li CI variables %li Any encrypted tokens - %hr - if can? current_user, :archive_project, @project + %hr .row.prepend-top-default .col-lg-3 %h4.warning-title.prepend-top-0 diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8c728eb0f6a..1f27d41ddd9 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,7 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do - = page_specific_javascript_tag("environments/environments_bundle.js") + = page_specific_javascript_bundle_tag("environments") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index f3179dce5f2..7800d6ac382 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -32,7 +32,7 @@ %tr %th ID %th Commit - %th Build + %th Job %th Created %th.hidden-xs diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 431253c1299..1d49e9cbaf7 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -4,7 +4,7 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" - = page_specific_javascript_tag("terminal/terminal_bundle.js") + = page_specific_javascript_bundle_tag("terminal") %div{ class: container_class } .top-area diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 1a62a6a809c..67018aaa2ac 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -5,8 +5,8 @@ %ul{ class: (container_class) } - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/chart.js') - = page_specific_javascript_tag('graphs/graphs_bundle.js') + = page_specific_javascript_bundle_tag('lib_chart') + = page_specific_javascript_bundle_tag('graphs') = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 431657c4dcb..b6f453b9736 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -1,4 +1,4 @@ -%h4 Build charts +%h4 Pipelines charts %p %span.cgreen @@ -11,19 +11,19 @@ .prepend-top-default %p.light - Builds for last week + Jobs for last week (#{date_from_to(Date.today - 7.days, Date.today)}) %canvas#weekChart{ height: 200 } .prepend-top-default %p.light - Builds for last month + Jobs for last month (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{ height: 200 } .prepend-top-default %p.light - Builds for last year + Jobs for last year %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 5fbed8b9ab8..8ea1a3a45e1 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -7,7 +7,7 @@ = render "projects/issues/head" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = page_specific_javascript_bundle_tag('filtered_search') = content_for :meta_tags do - if current_user diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 11636d7ebc7..a2305f4f547 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -3,7 +3,7 @@ - page_description @issue.description - page_card_attributes @issue.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/vue_resource.js') + = page_specific_javascript_bundle_tag('lib_vue') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9585a9a3ad4..b46c4a13cc4 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,8 +3,8 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/vue_resource.js') - = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') + = page_specific_javascript_bundle_tag('lib_vue') + = page_specific_javascript_bundle_tag('diff_notes') .merge-request{ 'data-url' => merge_request_path(@merge_request) } = render "projects/merge_requests/show/mr_title" @@ -108,10 +108,10 @@ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript - var merge_request; - - merge_request = new MergeRequest({ - action: "#{controller.action_name}" + $(function () { + new MergeRequest({ + action: "#{controller.action_name}" + }); }); var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index ebef2157d34..dcf578b85f9 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,7 +1,7 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/vue_resource.js') - = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') + = page_specific_javascript_bundle_tag('lib_vue') + = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 2595ce74ac0..0839880713f 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } + %a.btn.view-file{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 804a4a2473b..ae134563ead 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -10,7 +10,7 @@ = ci_label_for_status(status) for = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? @@ -21,7 +21,7 @@ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } = ci_icon_for_status(status) %span - CI build + CI job = ci_label_for_status(status) for - commit = @merge_request.diff_head_commit diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 38328501ffd..5de59473840 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -16,14 +16,18 @@ gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" + normal: "Job {{status}} for \"{{title}}\"", + preparing: "{{status}} job for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" + preparing: "{{status}} job", + normal: "Job {{status}}" }, + ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", + ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, + commits_path: "#{project_commits_path(@project)}", + pipeline_path: "#{project_pipelines_path(@project)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 7809e9c8c72..b730ced4214 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil @@ -35,10 +35,10 @@ The source branch will be removed. - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do = check_box_tag :should_remove_source_branch Remove source branch - .accept-control.right + .accept-control = link_to "#", class: "modify-merge-commit-link js-toggle-button" do = icon('edit') Modify commit message diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml index 14f51af5360..a18c2ad768f 100644 --- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -1,6 +1,6 @@ %h4 = icon('exclamation-triangle') - The build for this merge request failed + The job for this merge request failed %p - Please retry the build or push a new commit to fix the failure. + Please retry the job or push a new commit to fix the failure. diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index 50086767446..909dc52fc06 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') %strong = icon("spinner spin") diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index f70cd09c5f4..cf7abf3756c 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} @@ -19,7 +19,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index d8951e69242..b88eef65cef 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,7 @@ - page_title "Network", @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/raphael.js') - = page_specific_javascript_tag('network/network_bundle.js') + = page_specific_javascript_bundle_tag('network') = render "projects/commits/head" = render "head" %div{ class: container_class } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 064e92b15eb..cd685f7d0eb 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -50,7 +50,7 @@ = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do = icon('bitbucket', text: 'Bitbucket') - unless bitbucket_import_configured? = render 'bitbucket_import_modal' diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index b10dd47709f..721a9b6beb5 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -11,9 +11,9 @@ - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span - Builds + Jobs - if project_nav_tab? :environments = nav_link(controller: %w(environments)) do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 6caa5f16dc6..a6cd2d83bd5 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -25,7 +25,7 @@ .well-segment.pipeline-info .icon-container = icon('clock-o') - = pluralize @pipeline.statuses.count(:id), "build" + = pluralize @pipeline.statuses.count(:id), "job" - if @pipeline.ref from = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 88af41aa835..53067cdcba4 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -5,7 +5,7 @@ Pipeline %li.js-builds-tab-link = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do - Builds + Jobs %span.badge.js-builds-counter= pipeline.statuses.count @@ -33,7 +33,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index df36279ed75..f776734556a 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -64,4 +64,4 @@ .vue-pipelines-index -= page_specific_javascript_tag('vue_pipelines_index/index.js') += page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 1f698558bce..18328c67f02 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -66,7 +66,7 @@ %span.input-group-addon / %p.help-block A regular expression that will be used to find the test coverage - output in the build trace. Leave blank to disable + output in the job trace. Leave blank to disable = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') .bs-callout.bs-callout-info %p Below are examples of regex for existing tools: diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 42e9bdbd30e..b3b419bd92d 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,6 +1,6 @@ - page_title "Protected branches" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') + = page_specific_javascript_bundle_tag('protected_branches') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 33a9a96183c..98e72f6c547 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .checkbox = f.check_box :active - %span.light Paused Runners don't accept new builds + %span.light Paused Runners don't accept new jobs .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 92957470070..d6f691d9c24 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -2,7 +2,7 @@ .light.prepend-top-default %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, and even on your local machine. @@ -12,14 +12,14 @@ %ul %li %span.label.label-success active - \- Runner is active and can process any new builds + \- Runner is active and can process any new jobs %li %span.label.label-danger paused - \- Runner is paused and will not receive any new builds + \- Runner is paused and will not receive any new jobs %hr -%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners +%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners .row .col-sm-6 = render 'specific_runners' diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 068a6610350..e2a5107a883 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -27,3 +29,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 216f70f5605..fb39028529d 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -3,4 +3,4 @@ %h3.page-title Edit Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 772a594269c..cfed3a79bc5 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -3,4 +3,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 6e5dd1b196d..b9c4e323430 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -67,7 +67,7 @@ In the %code .gitlab-ci.yml of another project, include the following snippet. - The project will be rebuilt at the end of the build. + The project will be rebuilt at the end of the job. %pre :plain @@ -86,12 +86,12 @@ :plain #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN %h5.prepend-top-default - Pass build variables + Pass job variables %p.light Add %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. With cURL: diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml index 0249e0c1bf1..06477aba103 100644 --- a/app/views/projects/variables/_content.html.haml +++ b/app/views/projects/variables/_content.html.haml @@ -5,4 +5,4 @@ %p So you can use them for passwords, secret keys or whatever you want. %p - The value of the variable can be visible in build log if explicitly asked to do so. + The value of the variable can be visible in job log if explicitly asked to do so. diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b42eaabb111..2ad06dcf25b 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,8 +38,9 @@ #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) + #js-add-issues-btn.pull-right.prepend-left-10 .dropdown.pull-right - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } @@ -91,5 +92,5 @@ new SubscriptionSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0a4de709fcd..cb92b2e97a7 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -43,6 +43,8 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form += render 'shared/issuable/form/merge_params', issuable: issuable + - if @merge_request_for_resolving_discussions .form-group .col-sm-10.col-sm-offset-2 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ec9bcaf63dd..77fc44fa5cc 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,6 +1,7 @@ - todo = issuable_todo(issuable) - content_for :page_specific_javascripts do - = page_specific_javascript_tag('issuable/issuable_bundle.js') + = page_specific_javascript_bundle_tag('issuable') + %aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) @@ -130,7 +131,7 @@ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| - = link_to_label(label, type: issuable.to_ability_name) + = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index b757893ea04..2793e7bcff4 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -19,12 +19,3 @@ - if issuable.new_record? = link_to 'Change branches', mr_change_branches_path(issuable) - -- if issuable.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? - Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml new file mode 100644 index 00000000000..03309722326 --- /dev/null +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -0,0 +1,16 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless issuable.is_a?(MergeRequest) +- return if issuable.closed_without_fork? + +-# This check is duplicated below, to avoid conflicts with EE. +- return unless issuable.can_remove_source_branch?(current_user) + +.form-group + .col-sm-10.col-sm-offset-2 + - if issuable.can_remove_source_branch?(current_user) + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + Remove source branch when merge request is accepted. diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0c788032020..56c0f7390a5 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,6 +1,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('snippet/snippet_bundle.js') + = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| @@ -11,7 +11,7 @@ .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor .form-group @@ -34,4 +34,3 @@ = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 13586a5a12a..37e2a377a69 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 = page_title %p - #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be + #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be used for binding events when something is happening within the project. .col-lg-9.append-bottom-default = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| @@ -66,9 +66,9 @@ = f.check_box :build_events, class: 'pull-left' .prepend-left-20 = f.label :build_events, class: 'list-label' do - %strong Build events + %strong Jobs events %p.light - This URL will be triggered when the build status changes + This URL will be triggered when the job status changes %li = f.check_box :pipeline_events, class: 'pull-left' .prepend-left-20 diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 95fc7198104..9a9a3ff9220 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if current_user = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -26,3 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 82f44a9a5c3..915bf98eb3e 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -2,4 +2,4 @@ %h3.page-title Edit Snippet %hr -= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level += render 'shared/snippets/form', url: snippet_path(@snippet) diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 79e2392490d..ca8afb4bb6a 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -2,4 +2,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: snippets_path(@snippet) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c3d33d49c1e..44254040e4e 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,8 +1,8 @@ - page_title @user.name - page_description @user.bio - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/d3.js') - = page_specific_javascript_tag('users/users_bundle.js') + = page_specific_javascript_bundle_tag('lib_d3') + = page_specific_javascript_bundle_tag('users') - header_title @user.name, user_path(@user) - @no_container = true diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index b9cd49985dc..f5ccc84c160 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,13 +33,15 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new.execute(project, after_sha, project, before_sha) + compare = CompareService.new(project, after_sha) + .execute(project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new.execute(project, before_sha, project, after_sha) + compare = CompareService.new(project, before_sha) + .execute(project, after_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/bin/teaspoon b/bin/teaspoon deleted file mode 100755 index 7c3b8dfc4ed..00000000000 --- a/bin/teaspoon +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -require 'bundler/setup' -load Gem.bin_path('teaspoon', 'teaspoon') diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml new file mode 100644 index 00000000000..12f2998d1c8 --- /dev/null +++ b/changelogs/unreleased/17662-rename-builds.yml @@ -0,0 +1,4 @@ +--- +title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere +merge_request: 8787 +author: diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml new file mode 100644 index 00000000000..965d0648adf --- /dev/null +++ b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml @@ -0,0 +1,4 @@ +--- +title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb +merge_request: +author: diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml new file mode 100644 index 00000000000..fd671d04a9f --- /dev/null +++ b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml @@ -0,0 +1,4 @@ +--- +title: Force new password after password reset via API +merge_request: +author: George Andrinopoulos diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml new file mode 100644 index 00000000000..dac90eaa34d --- /dev/null +++ b/changelogs/unreleased/25460-replace-word-users-with-members.yml @@ -0,0 +1,4 @@ +--- +title: Replace word user with member +merge_request: 8872 +author: diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml new file mode 100644 index 00000000000..d7f950d7be9 --- /dev/null +++ b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml @@ -0,0 +1,4 @@ +--- +title: Remove turbolinks. +merge_request: !8570 +author: diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml new file mode 100644 index 00000000000..f74e9fa8b6d --- /dev/null +++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml @@ -0,0 +1,4 @@ +--- +title: Update pipeline and commit links when CI status is updated +merge_request: 8351 +author: diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml new file mode 100644 index 00000000000..1758ed9e9ea --- /dev/null +++ b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml @@ -0,0 +1,4 @@ +--- +title: Support non-ASCII characters in GFM autocomplete +merge_request: 8729 +author: diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml new file mode 100644 index 00000000000..7b307b501f4 --- /dev/null +++ b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index +merge_request: 8956 +author: diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml new file mode 100644 index 00000000000..293aab67d39 --- /dev/null +++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml @@ -0,0 +1,4 @@ +--- +title: Unify MR diff file button style +merge_request: 8874 +author: diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml new file mode 100644 index 00000000000..502927cd160 --- /dev/null +++ b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml @@ -0,0 +1,4 @@ +--- +title: Only render hr when user can't archive project. +merge_request: !8917 +author: diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml new file mode 100644 index 00000000000..79316abbaf7 --- /dev/null +++ b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline graph vertical spacing in Firefox and Safari +merge_request: 8886 +author: diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml new file mode 100644 index 00000000000..bc990c66866 --- /dev/null +++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong call to ProjectCacheWorker.perform +merge_request: 8910 +author: diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml new file mode 100644 index 00000000000..9fd6ea5bc52 --- /dev/null +++ b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml @@ -0,0 +1,4 @@ +--- +title: Adds /target_branch slash command functionality for merge requests +merge_request: +author: YarNayar diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml new file mode 100644 index 00000000000..61817027720 --- /dev/null +++ b/changelogs/unreleased/fix-depr-warn.yml @@ -0,0 +1,4 @@ +--- +title: resolve deprecation warnings +merge_request: 8855 +author: Adam Pahlevi diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml new file mode 100644 index 00000000000..e98ac755b88 --- /dev/null +++ b/changelogs/unreleased/fix-scroll-test.yml @@ -0,0 +1,4 @@ +--- +title: Change rspec test to guarantee window is resized before visiting page +merge_request: +author: diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml new file mode 100644 index 00000000000..1427e4e7624 --- /dev/null +++ b/changelogs/unreleased/fwn-to-find-by-full-path.yml @@ -0,0 +1,4 @@ +--- +title: replace `find_with_namespace` with `find_by_full_path` +merge_request: 8949 +author: Adam Pahlevi diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml new file mode 100644 index 00000000000..b2959c02c07 --- /dev/null +++ b/changelogs/unreleased/git_to_html_redirection.yml @@ -0,0 +1,4 @@ +--- +title: Redirect http://someproject.git to http://someproject +merge_request: +author: blackst0ne diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml new file mode 100644 index 00000000000..7f372ccb428 --- /dev/null +++ b/changelogs/unreleased/go-go-gadget-webpack.yml @@ -0,0 +1,4 @@ +--- +title: use webpack to bundle frontend assets and use karma for frontend testing +merge_request: 7288 +author: diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml new file mode 100644 index 00000000000..c11c2d4ede1 --- /dev/null +++ b/changelogs/unreleased/group-label-sidebar-link.yml @@ -0,0 +1,4 @@ +--- +title: Fixed group label links in issue/merge request sidebar +merge_request: +author: diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml new file mode 100644 index 00000000000..60da1c14702 --- /dev/null +++ b/changelogs/unreleased/issue-20428.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to define a coverage regex in the .gitlab-ci.yml +merge_request: 7447 +author: Leandro Camargo diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml new file mode 100644 index 00000000000..c855f0cbcf7 --- /dev/null +++ b/changelogs/unreleased/markdown-plantuml.yml @@ -0,0 +1,4 @@ +--- +title: PlantUML support for Markdown +merge_request: 8588 +author: Horacio Sanson diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml new file mode 100644 index 00000000000..e6ffb94bd08 --- /dev/null +++ b/changelogs/unreleased/slash-commands-typo.yml @@ -0,0 +1,4 @@ +--- +title: Fixed "substract" typo on /help/user/project/slash_commands +merge_request: 8976 +author: Jason Aquino diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml new file mode 100644 index 00000000000..4867f088953 --- /dev/null +++ b/changelogs/unreleased/snippet-spam.yml @@ -0,0 +1,4 @@ +--- +title: Check public snippets for spam +merge_request: +author: diff --git a/config/application.rb b/config/application.rb index f00e58a36ca..9088d3c432b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -80,6 +80,14 @@ module Gitlab # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + # Configure webpack + config.webpack.config_file = "config/webpack.config.js" + config.webpack.output_dir = "public/assets/webpack" + config.webpack.public_path = "assets/webpack" + + # Webpack dev server configuration is handled in initializers/static_files.rb + config.webpack.dev_server.enabled = false + # Enable the asset pipeline config.assets.enabled = true config.assets.paths << Gemojione.images_path @@ -88,31 +96,13 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" - config.assets.precompile << "lib/vue_resource.js" config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" - config.assets.precompile << "graphs/graphs_bundle.js" - config.assets.precompile << "users/users_bundle.js" - config.assets.precompile << "network/network_bundle.js" - config.assets.precompile << "profile/profile_bundle.js" - config.assets.precompile << "protected_branches/protected_branches_bundle.js" - config.assets.precompile << "diff_notes/diff_notes_bundle.js" - config.assets.precompile << "merge_request_widget/ci_bundle.js" - config.assets.precompile << "issuable/issuable_bundle.js" - config.assets.precompile << "boards/boards_bundle.js" - config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" - config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" - config.assets.precompile << "boards/test_utils/simulate_drag.js" - config.assets.precompile << "environments/environments_bundle.js" - config.assets.precompile << "blob_edit/blob_edit_bundle.js" - config.assets.precompile << "snippet/snippet_bundle.js" - config.assets.precompile << "terminal/terminal_bundle.js" - config.assets.precompile << "filtered_search/filtered_search_bundle.js" - config.assets.precompile << "lib/utils/*.js" - config.assets.precompile << "lib/*.js" + config.assets.precompile << "lib/ace.js" + config.assets.precompile << "lib/cropper.js" + config.assets.precompile << "lib/raphael.js" config.assets.precompile << "u2f.js" - config.assets.precompile << "vue_pipelines_index/index.js" config.assets.precompile << "vendor/assets/fonts/*" # Version of your assets, change this if you want to expire all your assets diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index c11296975b7..aabe859730a 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -1,9 +1,9 @@ --- -# IGNORED GROUPS AND GEMS - - :ignore_group - development - :who: Connor Shea - :why: Development gems are not distributed with the final product and are therefore exempt. + :why: Development gems are not distributed with the final product and are therefore + exempt. :versions: [] :when: 2016-04-17 21:27:01.054140000 Z - - :ignore_group @@ -18,8 +18,6 @@ :why: Bundler is MIT licensed but will sometimes fail in CI. :versions: [] :when: 2016-05-02 06:42:08.045090000 Z - -# LICENSE WHITELIST - - :whitelist - MIT - :who: Connor Shea @@ -86,9 +84,6 @@ :why: https://opensource.org/licenses/BSD-2-Clause :versions: [] :when: 2016-07-26 21:24:07.248480000 Z - - -# LICENSE BLACKLIST - - :blacklist - GPLv2 - :who: Connor Shea @@ -107,9 +102,6 @@ :why: The OSL license is a copyleft license :versions: [] :when: 2016-10-28 11:02:15.540105000 Z - - -# GEM LICENSES - - :license - raphael-rails - MIT @@ -201,3 +193,112 @@ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z +- - :approve + - after + - :who: Matt Lee + :why: https://github.com/Raynos/after/blob/master/LICENCE + :versions: [] + :when: 2017-01-14 20:00:32.473125000 Z +- - :approve + - amdefine + - :who: Matt Lee + :why: MIT License + :versions: [] + :when: 2017-01-14 20:08:31.810633000 Z +- - :approve + - base64id + - :who: Matt Lee + :why: https://github.com/faeldt/base64id/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:33.174760000 Z +- - :approve + - blob + - :who: Matt Lee + :why: https://github.com/webmodules/blob/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:34.564048000 Z +- - :approve + - callsite + - :who: Matt Lee + :why: https://github.com/tj/callsite/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:35.976025000 Z +- - :approve + - component-bind + - :who: Matt Lee + :why: https://github.com/component/bind/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:37.291219000 Z +- - :approve + - component-inherit + - :who: Matt Lee + :why: https://github.com/component/inherit/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:41.804804000 Z +- - :approve + - fsevents + - :who: Matt Lee + :why: https://github.com/strongloop/fsevents/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:50:20.037775000 Z +- - :approve + - indexof + - :who: Matt Lee + :why: https://github.com/component/indexof/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:43.209900000 Z +- - :approve + - is-integer + - :who: Matt Lee + :why: https://github.com/parshap/js-is-integer/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:44.540916000 Z +- - :approve + - jsonify + - :who: Matt Lee + :why: Public Domain - no formal license on this one. probably okay as its been + the same for along time. would prefer to see CC0 + :versions: [] + :when: 2017-01-14 20:10:45.857261000 Z +- - :approve + - object-component + - :who: Matt Lee + :why: https://github.com/component/object/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:47.190148000 Z +- - :approve + - optimist + - :who: Matt Lee + :why: https://github.com/substack/node-optimist/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:48.563077000 Z +- - :approve + - path-is-inside + - :who: Matt Lee + :why: https://github.com/domenic/path-is-inside/blob/master/LICENSE.txt + :versions: [] + :when: 2017-01-14 20:10:49.910497000 Z +- - :approve + - rc + - :who: Matt Lee + :why: https://github.com/dominictarr/rc/blob/master/LICENSE.MIT + :versions: [] + :when: 2017-01-14 20:10:51.244695000 Z +- - :approve + - ripemd160 + - :who: Matt Lee + :why: https://github.com/crypto-browserify/ripemd160/blob/master/LICENSE.md + :versions: [] + :when: 2017-01-14 20:10:52.560282000 Z +- - :approve + - select2 + - :who: Matt Lee + :why: https://github.com/select2/select2/blob/master/LICENSE.md + :versions: [] + :when: 2017-01-14 20:10:53.909618000 Z +- - :approve + - tweetnacl + - :who: Matt Lee + :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:57.812077000 Z diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 42e5f105d46..2906633fcbc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -505,6 +505,16 @@ production: &base # Git timeout to read a commit, in seconds timeout: 10 + ## Webpack settings + # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running + # on a given port instead of serving directly from /assets/webpack. This is only indended for use + # in development. + webpack: + # dev_server: + # enabled: true + # host: localhost + # port: 3808 + # # 5. Extra customization # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4f33aad8693..ea61aa9e047 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -410,6 +410,15 @@ Settings['gitaly'] ||= Settingslogic.new({}) Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH'] # +# Webpack settings +# +Settings['webpack'] ||= Settingslogic.new({}) +Settings.webpack['dev_server'] ||= Settingslogic.new({}) +Settings.webpack.dev_server['enabled'] ||= false +Settings.webpack.dev_server['host'] ||= 'localhost' +Settings.webpack.dev_server['port'] ||= 3808 + +# # Testing settings # if Rails.env.test? diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb new file mode 100644 index 00000000000..e8a77b146fa --- /dev/null +++ b/config/initializers/plantuml_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Plantuml diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb index a9aa802681a..fb5a7b8372e 100644 --- a/config/initializers/request_profiler.rb +++ b/config/initializers/request_profiler.rb @@ -1,5 +1,3 @@ -require 'gitlab/request_profiler/middleware' - Rails.application.configure do |config| config.middleware.use(Gitlab::RequestProfiler::Middleware) end diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d6dbf8b9fbf..74aba6c5d06 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -12,4 +12,35 @@ if app.config.serve_static_files app.paths["public"].first, app.config.static_cache_control ) + + # If webpack-dev-server is configured, proxy webpack's public directory + # instead of looking for static assets + dev_server = Gitlab.config.webpack.dev_server + + if dev_server.enabled + settings = { + enabled: true, + host: dev_server.host, + port: dev_server.port, + manifest_host: dev_server.host, + manifest_port: dev_server.port, + } + + if Rails.env.development? + settings.merge!( + host: Gitlab.config.gitlab.host, + port: Gitlab.config.gitlab.port, + https: Gitlab.config.gitlab.https, + ) + app.config.middleware.insert_before( + Gitlab::Middleware::Static, + Gitlab::Middleware::WebpackProxy, + proxy_path: app.config.webpack.public_path, + proxy_host: dev_server.host, + proxy_port: dev_server.port, + ) + end + + app.config.webpack.dev_server.merge!(settings) + end end diff --git a/config/karma.config.js b/config/karma.config.js new file mode 100644 index 00000000000..44229e2ee88 --- /dev/null +++ b/config/karma.config.js @@ -0,0 +1,21 @@ +var path = require('path'); +var webpackConfig = require('./webpack.config.js'); +var ROOT_PATH = path.resolve(__dirname, '..'); + +// Karma configuration +module.exports = function(config) { + config.set({ + basePath: ROOT_PATH, + browsers: ['PhantomJS'], + frameworks: ['jasmine'], + files: [ + { pattern: 'spec/javascripts/test_bundle.js', watched: false }, + { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, + ], + preprocessors: { + 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'], + }, + webpack: webpackConfig, + webpackMiddleware: { stats: 'errors-only' }, + }); +}; diff --git a/config/routes/project.rb b/config/routes/project.rb index f36febc6e04..7cd4a73b1a0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' + post :mark_as_spam end end @@ -266,7 +267,7 @@ constraints(ProjectUrlConstrainer.new) do resources :boards, only: [:index, :show] do scope module: :boards do - resources :issues, only: [:update] + resources :issues, only: [:index, :update] resources :lists, only: [:index, :create, :update, :destroy] do collection do diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 3ca096f31ba..ce0d1314292 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do member do get 'raw' get 'download' + post :mark_as_spam end end diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 00000000000..7cd92af7d93 --- /dev/null +++ b/config/webpack.config.js @@ -0,0 +1,125 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var webpack = require('webpack'); +var StatsPlugin = require('stats-webpack-plugin'); +var CompressionPlugin = require('compression-webpack-plugin'); + +var ROOT_PATH = path.resolve(__dirname, '..'); +var IS_PRODUCTION = process.env.NODE_ENV === 'production'; +var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; +var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; + +var config = { + context: path.join(ROOT_PATH, 'app/assets/javascripts'), + entry: { + application: './application.js', + blob_edit: './blob_edit/blob_edit_bundle.js', + boards: './boards/boards_bundle.js', + boards_test: './boards/test_utils/simulate_drag.js', + cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', + diff_notes: './diff_notes/diff_notes_bundle.js', + environments: './environments/environments_bundle.js', + filtered_search: './filtered_search/filtered_search_bundle.js', + graphs: './graphs/graphs_bundle.js', + issuable: './issuable/issuable_bundle.js', + merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', + merge_request_widget: './merge_request_widget/ci_bundle.js', + network: './network/network_bundle.js', + profile: './profile/profile_bundle.js', + protected_branches: './protected_branches/protected_branches_bundle.js', + snippet: './snippet/snippet_bundle.js', + terminal: './terminal/terminal_bundle.js', + users: './users/users_bundle.js', + lib_chart: './lib/chart.js', + lib_d3: './lib/d3.js', + lib_vue: './lib/vue_resource.js', + vue_pipelines: './vue_pipelines_index/index.js', + }, + + output: { + path: path.join(ROOT_PATH, 'public/assets/webpack'), + publicPath: '/assets/webpack/', + filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' + }, + + devtool: 'inline-source-map', + + module: { + loaders: [ + { + test: /\.es6$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + // 'use strict' was broken in sprockets-es6 due to sprockets concatination method. + // many es5 strict errors which were never caught ended up in our es6 assets as a result. + // this hack is necessary until they can be fixed. + blacklist: ['useStrict'] + } + }, + { + test: /\.(js|es6)$/, + loader: 'imports-loader', + query: 'this=>window' + }, + { + test: /\.json$/, + loader: 'json-loader' + } + ] + }, + + plugins: [ + // manifest filename must match config.webpack.manifest_filename + // webpack-rails only needs assetsByChunkName to function properly + new StatsPlugin('manifest.json', { + chunkModules: false, + source: false, + chunks: false, + modules: false, + assets: true + }), + new CompressionPlugin({ + asset: '[path].gz[query]', + }), + ], + + resolve: { + extensions: ['', '.js', '.es6', '.js.es6'], + alias: { + '~': path.join(ROOT_PATH, 'app/assets/javascripts'), + '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$': 'vue/dist/vue.js', + 'vue-resource$': 'vue-resource/dist/vue-resource.js' + } + } +} + +if (IS_PRODUCTION) { + config.devtool = 'source-map'; + config.plugins.push( + new webpack.NoErrorsPlugin(), + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false } + }), + new webpack.DefinePlugin({ + 'process.env': { NODE_ENV: JSON.stringify('production') } + }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.OccurrenceOrderPlugin() + ); +} + +if (IS_DEV_SERVER) { + config.devServer = { + port: DEV_SERVER_PORT, + headers: { 'Access-Control-Allow-Origin': '*' } + }; + config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; +} + +module.exports = config; diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index c04afe97277..c304e0706dc 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -26,7 +26,7 @@ Gitlab::Seeder.quiet do end end - project = Project.find_with_namespace('gitlab-org/gitlab-test') + project = Project.find_by_full_path('gitlab-org/gitlab-test') params = { source_branch: 'feature', diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb new file mode 100644 index 00000000000..88aa5d52b39 --- /dev/null +++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCoverageRegexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :coverage_regex, :string + end +end diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb new file mode 100644 index 00000000000..0ee4229d1f8 --- /dev/null +++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb @@ -0,0 +1,17 @@ +class RemoveBacklogListsFromBoards < ActiveRecord::Migration + DOWNTIME = false + + def up + execute <<-SQL + DELETE FROM lists WHERE list_type = 0; + SQL + end + + def down + execute <<-SQL + INSERT INTO lists (board_id, list_type, created_at, updated_at) + SELECT boards.id, 0, NOW(), NOW() + FROM boards; + SQL + end +end diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb new file mode 100644 index 00000000000..8f944930807 --- /dev/null +++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb @@ -0,0 +1,11 @@ +class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :labels, [:type, :project_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index 5efb4f6595c..92b36218a15 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170130204620) do +ActiveRecord::Schema.define(version: 20170204181513) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.datetime "queued_at" t.string "token" t.integer "lock_version" + t.string "coverage_regex" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -575,6 +576,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false diff --git a/doc/README.md b/doc/README.md index 909740211a6..d5f0c37325e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,10 +18,10 @@ - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) -- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. +- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. +- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. - [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file. - [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations. diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 80e5d80aa41..4d35b20d0c3 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -3,7 +3,7 @@ > **Note:** Custom Git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not +Please explore [webhooks] as an option if you do not have filesystem access. For a user configurable Git hook interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html). @@ -80,5 +80,6 @@ STDERR takes precedence over STDOUT. ![Custom message from custom Git hook](img/custom_hooks_error_msg.png) [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks +[webhooks]: ../user/project/integrations/webhooks.md [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 [93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index e5cf592e0a6..6515b1a264a 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -3,8 +3,8 @@ > [Introduced][ce-7810] in GitLab 8.16. When [PlantUML](http://plantuml.com) integration is enabled and configured in -GitLab we are able to create simple diagrams in AsciiDoc documents created in -snippets, wikis, and repos. +GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents +created in snippets, wikis, and repos. ## PlantUML Server @@ -54,7 +54,7 @@ that, login with an Admin account and do following: ## Creating Diagrams With PlantUML integration enabled and configured, we can start adding diagrams to -our AsciiDoc snippets, wikis and repos using blocks: +our AsciiDoc snippets, wikis and repos using delimited blocks: ``` [plantuml, format="png", id="myDiagram", width="200px"] @@ -64,7 +64,14 @@ Alice -> Bob : Go Away -- ``` -The above block will be converted to an HTML img tag with source pointing to the +And in Markdown using fenced code blocks: + + ```plantuml + Bob -> Alice : hello + Alice -> Bob : Go Away + ``` + +The above blocks will be converted to an HTML img tag with source pointing to the PlantUML instance. If the PlantUML server is correctly configured, this should render a nice diagram instead of the block: @@ -77,7 +84,7 @@ Inside the block you can add any of the supported diagrams by PlantUML such as and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block. -Some parameters can be added to the block definition: +Some parameters can be added to the AsciiDoc block definition: - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by all browsers so use with care. The default is `png`. @@ -85,3 +92,4 @@ Some parameters can be added to the block definition: - *width*: Width attribute added to the img tag. - *height*: Height attribute added to the img tag. +Markdown does not support any parameters and will always use PNG format. diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index a1d1bb03b50..3fbb13704aa 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,13 +1,12 @@ # Web terminals -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) -in GitLab 8.15. Only project masters and owners can access web terminals. +> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can + access web terminals. -With the introduction of the [Kubernetes](../../project_services/kubernetes.md) -project service, GitLab gained the ability to store and use credentials for a -Kubernetes cluster. One of the things it uses these credentials for is providing -access to [web terminals](../../ci/environments.html#web-terminals) -for environments. +With the introduction of the [Kubernetes project service][kubservice], GitLab +gained the ability to store and use credentials for a Kubernetes cluster. One +of the things it uses these credentials for is providing access to +[web terminals](../../ci/environments.html#web-terminals) for environments. ## How it works @@ -71,3 +70,6 @@ by the above guides. When these headers are not passed through, Workhorse will return a `400 Bad Request` response to users attempting to use a web terminal. In turn, they will receive a `Connection failed` message. + +[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690 +[kubservice]: ../../user/project/integrations/kubernetes.md) diff --git a/doc/api/services.md b/doc/api/services.md index 1466b8189b0..fba5da6587d 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -808,5 +808,5 @@ Get JetBrains TeamCity CI service settings for a project. GET /projects/:id/services/teamcity ``` -[jira-doc]: ../project_services/jira.md +[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 diff --git a/doc/api/users.md b/doc/api/users.md index 28b6c7bd491..fea9bdf9639 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -271,6 +271,7 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) +On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, even in cases where a `409` (Conflict) would be more appropriate, e.g. when renaming the email address to some existing one. diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index c4c4d95b68a..4028a5efa9e 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -34,8 +34,8 @@ created automatically for you. [mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 [project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html -[project-services]: ../../project_services/project_services.md +[project-services]: ../../user/project/integrations/project_services.md [auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy -[kubernetes-service]: ../../project_services/kubernetes.md +[kubernetes-service]: ../../user/project/integrations/kubernetes.md [docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor [review-app]: ../review_apps/index.md diff --git a/doc/ci/environments.md b/doc/ci/environments.md index ef04c537367..579135c2052 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -1,7 +1,6 @@ # Introduction to environments and deployments ->**Note:** -Introduced in GitLab 8.9. +> Introduced in GitLab 8.9. During the development of software, there can be many stages until it's ready for public consumption. You sure want to first test your code and then deploy it @@ -242,7 +241,7 @@ Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open +the [Kubernetes service][kubernetes-service], GitLab can open a terminal session to your environment! This is a very powerful feature that allows you to debug issues without leaving the comfort of your web browser. To enable it, just follow the instructions given in the service documentation. @@ -566,7 +565,7 @@ Below are some links you may find interesting: [Pipelines]: pipelines.md [jobs]: yaml/README.md#jobs [yaml]: yaml/README.md -[kubernetes-service]: ../project_services/kubernetes.md] +[kubernetes-service]: ../user/project/integrations/kubernetes.md [environments]: #environments [deployments]: #deployments [permissions]: ../user/permissions.md diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index c40cdd55ea5..1104edaabe9 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -217,7 +217,7 @@ builds, you should explicitly enable the **Builds Emails** service under your project's settings. For more information read the -[Builds emails service documentation](../../project_services/builds_emails.md). +[Builds emails service documentation](../../user/project/integrations/builds_emails.md). ## Examples diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d3b9611b02e..49fca884f35 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -157,14 +157,14 @@ Once you set them, they will be available for all subsequent builds. >**Note:** This feature requires GitLab CI 8.15 or higher. -[Project services](../../project_services/project_services.md) that are +[Project services](../../user/project/integrations/project_services.md) that are responsible for deployment configuration may define their own variables that are set in the build environment. These variables are only defined for [deployment builds](../environments.md). Please consult the documentation of the project services that you are using to learn which variables they define. An example project service that defines deployment variables is -[Kubernetes Service](../../project_services/kubernetes.md). +[Kubernetes Service](../../user/project/integrations/kubernetes.md). ## Debug tracing diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f11257be5c3..06810898cfe 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | +| coverage | no | Define coverage settings for all jobs | ### image and services @@ -278,6 +279,23 @@ cache: untracked: true ``` +### coverage + +`coverage` allows you to configure how coverage will be filtered out from the +build outputs. Setting this up globally will make all the jobs to use this +setting for output filtering and extracting the coverage information from your +builds. + +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. + +A simple example: +```yaml +coverage: /\(\d+\.\d+\) covered\./ +``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -319,6 +337,7 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | +| coverage | no | Define coverage settings for a given job | ### script @@ -993,6 +1012,25 @@ job: - execute this after my script ``` +### job coverage + +This entry is pretty much the same as described in the global context in +[`coverage`](#coverage). The only difference is that, by setting it inside +the job level, whatever is set in there will take precedence over what has +been defined in the global level. A quick example of one overriding the +other would be: + +```yaml +coverage: /\(\d+\.\d+\) covered\./ + +job1: + coverage: /Code coverage: \d+\.\d+/ +``` + +In the example above, considering the context of the job `job1`, the coverage +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. + ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 903e54bf9dc..5dae4bcc905 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net]( ### Hover -Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. +Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect. View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 1b19587a0b8..18d0647c798 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -96,6 +96,20 @@ Since secondary buttons only have a border on their resting state, their hover a | Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) | | Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) | +### Placement + +When there are a group of buttons in a dialog or a form, we need to be consistent with the placement. + +#### Dismissive actions on the left +The dismissive action returns the user to the previous state. + +> Example: Cancel + +#### Affirmative actions on the right +Affirmative actions continue to progress towards the user goal that triggered the dialog or form. + +> Example: Submit, Ok, Delete + --- diff --git a/doc/install/README.md b/doc/install/README.md index 239f5f301ec..2d2fd8cb380 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -4,3 +4,6 @@ - [Requirements](requirements.md) - [Structure](structure.md) - [Database MySQL](database_mysql.md) +- [Digital Ocean and Docker](digitaloceandocker.md) +- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker) +- [All installation methods](https://about.gitlab.com/installation/) diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md new file mode 100644 index 00000000000..820060a489b --- /dev/null +++ b/doc/install/digitaloceandocker.md @@ -0,0 +1,136 @@ +# Digital Ocean and Docker + +## Initial setup + +In this guide you'll configure a Digital Ocean droplet and set up Docker +locally on either macOS or Linux. + +### On macOS + +#### Install Docker Toolbox + +1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox) + +### On Linux + +#### Install Docker Engine + +1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/) + +#### Install Docker Machine + +1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/) + +_The rest of the steps are identical for macOS and Linux_ + +### Create new docker host + +1. Login to Digital Ocean +1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens + + +This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host. + +**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance** + ++ RAM: 4GB ++ Name: `gitlab-test-env-do` ++ Driver: `digitalocean` + + +**Set the DO token** - Replace the string below with your generated token + +``` +export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248 +``` + +**Create the machine** + +``` +docker-machine create \ + --driver digitalocean \ + --digitalocean-access-token=$DOTOKEN \ + --digitalocean-size "4gb" \ + gitlab-test-env-do +``` + ++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/ + + +### Creating GitLab test instance + + +#### Connect your shell to the new machine + + +In this example we'll create a GitLab EE 8.10.8 instance. + + +First connect the docker client to the docker host you created previously. + +``` +eval "$(docker-machine env gitlab-test-env-do)" +``` + +You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host + + +#### Create new GitLab container + ++ HTTP port: `8888` ++ SSH port: `2222` + + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG ` ++ Hostname: IP of docker host ++ Container name: `gitlab-test-8.10` ++ GitLab version: **EE** `8.10.8-ee.0` + +##### Setup container settings + +``` +export SSH_PORT=2222 +export HTTP_PORT=8888 +export VERSION=8.10.8-ee.0 +export NAME=gitlab-test-8.10 +``` + +##### Create container +``` +docker run --detach \ +--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \ +--hostname $(docker-machine ip gitlab-test-env-do) \ +-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \ +--name $NAME \ +gitlab/gitlab-ee:$VERSION +``` + +#### Connect to the GitLab container + +##### Retrieve the docker host IP + +``` +docker-machine ip gitlab-test-env-do +# example output: 192.168.151.134 +``` + + ++ Browse to: http://192.168.151.134:8888/ + + +##### Execute interactive shell/edit configuration + + +``` +docker exec -it $NAME /bin/bash +``` + +``` +# example commands +root@192:/# vi /etc/gitlab/gitlab.rb +root@192:/# gitlab-ctl reconfigure +``` + +#### Resources + ++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/) ++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/) ++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+ diff --git a/doc/install/installation.md b/doc/install/installation.md index 425c5d93efb..b2d5d51d37d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -124,7 +124,7 @@ Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz - echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz + echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz cd ruby-2.3.3 ./configure --disable-install-rdoc make diff --git a/doc/integration/README.md b/doc/integration/README.md index e97430feb57..22bdf33443d 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID @@ -18,17 +18,14 @@ See the documentation below for details on how to configure these services. - [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration - [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. -GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. - -[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html - +> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. -[Project Service]: ../project_services/project_services.md +[Project Service]: ../user/project/integrations/project_services.md ## SSL certificate errors @@ -64,3 +61,5 @@ After that restart GitLab with: ```bash sudo gitlab-ctl restart ``` + +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 8d2c6351fb8..265c891cf83 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -18,9 +18,9 @@ The configuration is done via a project's **Services**. To enable an external issue tracker you must configure the appropriate **Service**. Visit the links below for details: -- [Redmine](../project_services/redmine.md) -- [Jira](../project_services/jira.md) -- [Bugzilla](../project_services/bugzilla.md) +- [Redmine](../user/project/integrations/redmine.md) +- [Jira](../user/project/integrations/jira.md) +- [Bugzilla](../user/project/integrations/bugzilla.md) ### Service Template @@ -28,4 +28,4 @@ To save you the hassle from configuring each project's service individually, GitLab provides the ability to set Service Templates which can then be overridden in each project's settings. -Read more on [Services Templates](../project_services/services_templates.md). +Read more on [Services Templates](../user/project/integrations/services_templates.md). diff --git a/doc/integration/jira.md b/doc/integration/jira.md index e2f136bcc35..b6923f74e28 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1 @@ -# GitLab JIRA integration - -This document was moved to [project_services/jira](../project_services/jira.md). +This document was moved to [integrations/jira](../user/project/integrations/jira.md). diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md index 51668128c62..5b171080c72 100644 --- a/doc/project_services/bamboo.md +++ b/doc/project_services/bamboo.md @@ -1,60 +1 @@ -# Atlassian Bamboo CI Service - -GitLab provides integration with Atlassian Bamboo for continuous integration. -When configured, pushes to a project will trigger a build in Bamboo automatically. -Merge requests will also display CI status showing whether the build is pending, -failed, or completed successfully. It also provides a link to the Bamboo build -page for more information. - -Bamboo doesn't quite provide the same features as a traditional build system when -it comes to accepting webhooks and commit data. There are a few things that -need to be configured in a Bamboo build plan before GitLab can integrate. - -## Setup - -### Complete these steps in Bamboo: - -1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions' -dropdown. -1. Select the 'Triggers' tab. -1. Click 'Add trigger'. -1. Enter a description such as 'GitLab trigger' -1. Choose 'Repository triggers the build when changes are committed' -1. Check one or more repositories checkboxes -1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a -whitelist of IP addresses that are allowed to trigger Bamboo builds. -1. Save the trigger. -1. In the left pane, select a build stage. If you have multiple build stages -you want to select the last stage that contains the git checkout task. -1. Select the 'Miscellaneous' tab. -1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}' -in the 'Labels' box. -1. Save - -Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo -service in GitLab - -### Complete these steps in GitLab: - -1. Navigate to the project you want to configure to trigger builds. -1. Select 'Settings' in the top navigation. -1. Select 'Services' in the left navigation. -1. Click 'Atlassian Bamboo CI' -1. Select the 'Active' checkbox. -1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' -1. Enter the build key from your Bamboo build plan. Build keys are a short, -all capital letter, identifier that is unique. It will be something like PR-BLD -1. If necessary, enter username and password for a Bamboo user that has -access to trigger the build plan. Leave these fields blank if you do not require -authentication. -1. Save or optionally click 'Test Settings'. Please note that 'Test Settings' -will actually trigger a build in Bamboo. - -## Troubleshooting - -If builds are not triggered, these are a couple of things to keep in mind. - -1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger -IP addresses'. -1. Remember that GitLab only triggers builds on push events. A commit via the -web interface will not trigger CI currently. +This document was moved to [user/project/integrations/bamboo.md](../user/project/integrations/bamboo.md). diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md index 215ed6fe9cc..e67055d5616 100644 --- a/doc/project_services/bugzilla.md +++ b/doc/project_services/bugzilla.md @@ -1,17 +1 @@ -# Bugzilla Service - -Go to your project's **Settings > Services > Bugzilla** and fill in the required -details as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | -| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | -| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | - -Once you have configured and enabled Bugzilla: - -- the **Issues** link on the GitLab project pages takes you to the appropriate - Bugzilla product page -- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue +This document was moved to [user/project/integrations/bugzilla.md](../user/project/integrations/bugzilla.md). diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md index af0b1a287c7..ee54d865225 100644 --- a/doc/project_services/builds_emails.md +++ b/doc/project_services/builds_emails.md @@ -1,16 +1 @@ -## Enabling build emails - -To receive e-mail notifications about the result status of your builds, visit -your project's **Settings > Services > Builds emails** and activate the service. - -In the _Recipients_ area, provide a list of e-mails separated by comma. - -Check the _Add pusher_ checkbox if you want the committer to also receive -e-mail notifications about each build's status. - -If you enable the _Notify only broken builds_ option, e-mail notifications will -be sent only for failed builds. - ---- - -![Builds emails service settings](img/builds_emails_service.png) +This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md). diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md index 2f9f36f962e..a2e831ada34 100644 --- a/doc/project_services/emails_on_push.md +++ b/doc/project_services/emails_on_push.md @@ -1,17 +1 @@ -## Enabling emails on push - -To receive email notifications for every change that is pushed to the project, visit -your project's **Settings > Services > Emails on push** and activate the service. - -In the _Recipients_ area, provide a list of emails separated by commas. - -You can configure any of the following settings depending on your preference. - -+ **Push events** - Email will be triggered when a push event is recieved -+ **Tag push events** - Email will be triggered when a tag is created and pushed -+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). -+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. - ---- - -![Email on push service settings](img/emails_on_push_service.png) +This document was moved to [user/project/integrations/emails_on_push.md](../user/project/integrations/emails_on_push.md). diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md index 021a93a288f..4ae9f6c6b2e 100644 --- a/doc/project_services/hipchat.md +++ b/doc/project_services/hipchat.md @@ -1,54 +1 @@ -# Atlassian HipChat - -GitLab provides a way to send HipChat notifications upon a number of events, -such as when a user pushes code, creates a branch or tag, adds a comment, and -creates a merge request. - -## Setup - -GitLab requires the use of a HipChat v2 API token to work. v1 tokens are -not supported at this time. Note the differences between v1 and v2 tokens: - -HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 -token is allowed to send messages to *any* room. - -HipChat v2 API has tokens that are can be created using the Integrations tab -in the Group or Room admin page. By design, these are lightweight tokens that -allow GitLab to send messages only to *one* room. - -### Complete these steps in HipChat: - -1. Go to: https://admin.hipchat.com/admin -1. Click on "Group Admin" -> "Integrations". -1. Find "Build Your Own!" and click "Create". -1. Select the desired room, name the integration "GitLab", and click "Create". -1. In the "Send messages to this room by posting this URL" column, you should -see a URL in the format: - -``` - https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> -``` - -HipChat is now ready to accept messages from GitLab. Next, set up the HipChat -service in GitLab. - -### Complete these steps in GitLab: - -1. Navigate to the project you want to configure for notifications. -1. Select "Settings" in the top navigation. -1. Select "Services" in the left navigation. -1. Click "HipChat". -1. Select the "Active" checkbox. -1. Insert the `token` field from the URL into the `Token` field on the Web page. -1. Insert the `room` field from the URL into the `Room` field on the Web page. -1. Save or optionally click "Test Settings". - -## Troubleshooting - -If you do not see notifications, make sure you are using a HipChat v2 API -token, not a v1 token. - -Note that the v2 token is tied to a specific room. If you want to be able to -specify arbitrary rooms, you can create an API token for a specific user in -HipChat under "Account settings" and "API access". Use the `XXX` value under -`auth_token=XXX`. +This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md). diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md index 25c0c3ad2a6..7f0850dcc24 100644 --- a/doc/project_services/irker.md +++ b/doc/project_services/irker.md @@ -1,51 +1 @@ -# Irker IRC Gateway - -GitLab provides a way to push update messages to an Irker server. When -configured, pushes to a project will trigger the service to send data directly -to the Irker server. - -See the project homepage for further info: https://gitlab.com/esr/irker - -## Needed setup - -You will first need an Irker daemon. You can download the Irker code from its -repository on https://gitlab.com/esr/irker: - -``` -git clone https://gitlab.com/esr/irker.git -``` - -Once you have downloaded the code, you can run the python script named `irkerd`. -This script is the gateway script, it acts both as an IRC client, for sending -messages to an IRC server obviously, and as a TCP server, for receiving messages -from the GitLab service. - -If the Irker server runs on the same machine, you are done. If not, you will -need to follow the firsts steps of the next section. - -## Complete these steps in GitLab: - -1. Navigate to the project you want to configure for notifications. -1. Select "Settings" in the top navigation. -1. Select "Services" in the left navigation. -1. Click "Irker". -1. Select the "Active" checkbox. -1. Enter the server host address where `irkerd` runs (defaults to `localhost`) -in the `Server host` field on the Web page -1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the -`Server port` field on the Web page. -1. Optional: if `Default IRC URI` is set, it has to be in the format -`irc[s]://domain.name` and will be prepend to each and every channel provided -by the user which is not a full URI. -1. Specify the recipients (e.g. #channel1, user1, etc.) -1. Save or optionally click "Test Settings". - -## Note on Irker recipients - -Irker accepts channel names of the form `chan` and `#chan`, both for the -`#chan` channel. If you want to send messages in query, you will need to add -`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter -case, `Aorimn` is treated as a nick and no more as a channel name. - -Irker can also join password-protected channels. Users need to append -`?key=thesecretpassword` to the chan name. +This document was moved to [user/project/integrations/irker.md](../user/project/integrations/irker.md). diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 390066c9989..63614feba82 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,208 +1 @@ -# GitLab JIRA integration - -GitLab can be configured to interact with JIRA. Configuration happens via -user name and password. Connecting to a JIRA server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance, see the -[configuration](#configuration) section. If you have one JIRA instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. - -Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab. - -## Configuration - -In order to enable the JIRA service in GitLab, you need to first configure the -project in JIRA and then enter the correct values in GitLab. - -### Configuring JIRA - -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. - -As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` -group. - -**It is important that the user `GitLab` has write-access to projects in JIRA** - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![jira create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access ➔ View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - ->**Notes:** -- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or - higher is required. -- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified - the configuration options you have to enter. If you are using an older version, - [follow this documentation][jira-repo-docs]. - -To enable JIRA integration in a project, navigate to your project's -**Services ➔ JIRA** and fill in the required details on the page as described -in the table below. - -| Field | Description | -| ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | -| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -![JIRA service page](img/jira_service_page.png) - ---- - -## JIRA issues - -By now you should have [configured JIRA](#configuring-jira) and enabled the -[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference and close JIRA issues by just mentioning their -ID in GitLab commits and merge requests. - -### Referencing JIRA Issues - -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the -format: - -``` -USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: -ENTITY_TITLE -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. -* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. -* `PROJECT_NAME` GitLab project name. -* `ENTITY_TITLE` Merge request title or commit message first line. - -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) - ---- - -### Closing JIRA Issues - -JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit which contains the trigger word -followed by the JIRA issue ID in the commit message is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it (provided -the transition ID was set up correctly). - -There are currently three trigger words, and you can use either one to achieve -the same goal: - -- `Resolves PROJECT-1` -- `Closes PROJECT-1` -- `Fixes PROJECT-1` - -where `PROJECT-1` is the issue ID of the JIRA project. - -### JIRA issue closing example - -Let's consider the following example: - -1. For the project named `PROJECT` in JIRA, we implemented a new feature - and created a merge request in GitLab. -1. This feature was requested in JIRA issue `PROJECT-7` and the merge request - in GitLab contains the improvement -1. In the merge request description we use the issue closing trigger - `Closes PROJECT-7`. -1. Once the merge request is merged, the JIRA issue will be automatically closed - with a comment and an associated link to the commit that resolved the issue. - ---- - -In the following screenshot you can see what the link references to the JIRA -issue look like. - -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) - ---- - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) - ---- - -![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) - -## Troubleshooting - -If things don't work as expected that's usually because you have configured -incorrectly the JIRA-GitLab integration. - -### GitLab is unable to comment on a ticket - -Make sure that the user you set up for GitLab to communicate with JIRA has the -correct access permission to post comments on a ticket and to also transition -the ticket, if you'd like GitLab to also take care of closing them. -JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. - -### GitLab is unable to close a ticket - -Make sure the `Transition ID` you set within the JIRA settings matches the one -your project needs to close a ticket. - -[services-templates]: ../project_services/services_templates.md -[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md +This document was moved to [user/project/integrations/jira.md](../user/project/integrations/jira.md). diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index 99aa9e44bdb..0497a13c2b7 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -1,63 +1 @@ -# GitLab Kubernetes / OpenShift integration - -GitLab can be configured to interact with Kubernetes, or other systems using the -Kubernetes API (such as OpenShift). - -Each project can be configured to connect to a different Kubernetes cluster, see -the [configuration](#configuration) section. - -If you have a single cluster that you want to use for all your projects, -you can pre-fill the settings page with a default template. To configure the -template, see the [Services Templates](services_templates.md) document. - -## Configuration - -![Kubernetes configuration settings](img/kubernetes_configuration.png) - -The Kubernetes service takes the following arguments: - -1. Kubernetes namespace -1. API URL -1. Service token -1. Custom CA bundle - -The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes -exposes several APIs - we want the "base" URL that is common to all of them, -e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. - -GitLab authenticates against Kubernetes using service tokens, which are -scoped to a particular `namespace`. If you don't have a service token yet, -you can follow the -[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) -to create one. You can also view or create service tokens in the -[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit -`Config -> Secrets`. - -Fill in the service token and namespace according to the values you just got. -If the API is using a self-signed TLS certificate, you'll also need to include -the `ca.crt` contents as the `Custom CA bundle`. - -## Deployment variables - -The Kubernetes service exposes following -[deployment variables](../ci/variables/README.md#deployment-variables) in the -GitLab CI build environment: - -- `KUBE_URL` - equal to the API URL -- `KUBE_TOKEN` -- `KUBE_NAMESPACE` -- `KUBE_CA_PEM` - only if a custom CA bundle was specified - -## Web terminals - ->**NOTE:** -Added in GitLab 8.15. You must be the project owner or have `master` permissions -to use terminals. Support is currently limited to the first container in the -first pod of your environment. - -When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) -support to your environments. This is based on the `exec` functionality found in -Docker and Kubernetes, so you get a new shell session within your existing -containers. To use this integration, you should deploy to Kubernetes using -the deployment variables above, ensuring any pods you create are labelled with -`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! +This document was moved to [user/project/integrations/kubernetes.md](../user/project/integrations/kubernetes.md). diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md index fbc7dfeee6d..554a028853e 100644 --- a/doc/project_services/mattermost.md +++ b/doc/project_services/mattermost.md @@ -1,45 +1 @@ -# Mattermost Notifications Service - -## On Mattermost - -To enable Mattermost integration you must create an incoming webhook integration: - -1. Sign in to your Mattermost instance -1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add -1. Choose a display name, description and channel, those can be overridden on GitLab -1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. - -There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable -it on https://mattermost.example/admin_console/integrations/custom. - -Display name override is not enabled by default, you need to ask your admin to enable it on that same section. - -## On GitLab - -After you set up Mattermost, it's time to set up GitLab. - -Go to your project's **Settings > Services > Mattermost Notifications** and you will see a -checkbox with the following events that can be triggered: - -- Push -- Issue -- Merge request -- Note -- Tag push -- Build -- 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. - -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... | -| **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. | - - -![Mattermost configuration](img/mattermost_configuration.png) +This document was moved to [user/project/integrations/mattermost.md](../user/project/integrations/mattermost.md). diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 67cb88104c1..7c238b5dc37 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -1,163 +1 @@ -# Mattermost slash commands - -> Introduced in GitLab 8.14 - -Mattermost commands give users an extra interface to perform common operations -from the chat environment. This allows one to, for example, create an issue as -soon as the idea was discussed in Mattermost. - -## Prerequisites - -Mattermost 3.4 and up is required. - -If you have the Omnibus GitLab package installed, Mattermost is already bundled -in it. All you have to do is configure it. Read more in the -[Omnibus GitLab Mattermost documentation][omnimmdocs]. - -## Automated Configuration - -If Mattermost is installed on the same server as GitLab, the configuration process can be -done for you by GitLab. - -Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. - -## Manual Configuration - -The configuration consists of two parts. First you need to enable the slash -commands in Mattermost and then enable the service in GitLab. - -### Step 1. Enable custom slash commands in Mattermost - -This step is only required when using a source install, omnibus installs will be -preconfigured with the right settings. - -The first thing to do in Mattermost is to enable custom slash commands from -the administrator console. - -1. Log in with an account that has admin privileges and navigate to the system - console. - - ![Mattermost go to console](img/mattermost_goto_console.png) - - --- - -1. Click **Custom integrations** and set **Enable Custom Slash Commands**, - **Enable custom integrations to override usernames**, and **Override - custom integrations to override profile picture icons** to true - - ![Mattermost console](img/mattermost_console_integrations.png) - - --- - -1. Click **Save** at the bottom to save the changes. - -### Step 2. Open the Mattermost slash commands service in GitLab - -1. Open a new tab for GitLab and go to your project's settings - **Services ➔ Mattermost command**. A screen will appear with all the values you - need to copy in Mattermost as described in the next step. Leave the window open. - - >**Note:** - GitLab will propose some values for the Mattermost settings. The only one - required to copy-paste as-is is the **Request URL**, all the others are just - suggestions. - - ![Mattermost setup instructions](img/mattermost_config_help.png) - - --- - -1. Proceed to the next step and create a slash command in Mattermost with the - above values. - -### Step 3. Create a new custom slash command in Mattermost - -Now that you have enabled custom slash commands in Mattermost and opened -the Mattermost slash commands service in GitLab, it's time to copy these values -in a new slash command. - -1. Back to Mattermost, under your team page settings, you should see the - **Integrations** option. - - ![Mattermost team integrations](img/mattermost_team_integrations.png) - - --- - -1. Go to the **Slash Commands** integration and add a new one by clicking the - **Add Slash Command** button. - - ![Mattermost add command](img/mattermost_add_slash_command.png) - - --- - -1. Fill in the options for the custom command as described in - [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab). - - >**Note:** - If you plan on connecting multiple projects, pick a slash command trigger - word that relates to your projects such as `/gitlab-project-name` or even - just `/project-name`. Only use `/gitlab` if you will only connect a single - project to your Mattermost team. - - ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png) - -1. After you setup all the values, copy the token (we will use it below) and - click **Done**. - - ![Mattermost slash command token](img/mattermost_slash_command_token.png) - -### Step 4. Copy the Mattermost token into the Mattermost slash command service - -1. In GitLab, paste the Mattermost token you copied in the previous step and - check the **Active** checkbox. - - ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png) - -1. Click **Save changes** for the changes to take effect. - ---- - -You are now set to start using slash commands in Mattermost that talk to the -GitLab project you configured. - -## Authorizing Mattermost to interact with GitLab - -The first time a user will interact with the newly created slash commands, -Mattermost will trigger an authorization process. - -![Mattermost bot authorize](img/mattermost_bot_auth.png) - -This will connect your Mattermost user with your GitLab user. You can -see all authorized chat accounts in your profile's page under **Chat**. - -When the authorization process is complete, you can start interacting with -GitLab using the Mattermost commands. - -## Available slash commands - -The available slash commands are: - -| Command | Description | Example | -| ------- | ----------- | ------- | -| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | -| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | -| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | - -To see a list of available commands to interact with GitLab, type the -trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> - -![Mattermost bot available commands](img/mattermost_bot_available_commands.png) - -## Permissions - -The permissions to run the [available commands](#available-commands) derive from -the [permissions you have on the project](../user/permissions.md#project). - -## Further reading - -- [Mattermost slash commands documentation][mmslashdocs] -- [Omnibus GitLab Mattermost][omnimmdocs] - - -[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ -[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html -[ciyaml]: ../ci/yaml/README.md +This document was moved to [user/project/integrations/mattermost_slash_commands.md](../user/project/integrations/mattermost_slash_commands.md). diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 547d855d777..2c555c4edae 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,59 +1 @@ -# Project Services - -Project services allow you to integrate GitLab with other applications. Below -is list of the currently supported ones. - -You can find these within GitLab in the Services page under Project Settings if -you are at least a master on the project. -Project Services are a bit like plugins in that they allow a lot of freedom in -adding functionality to GitLab. For example there is also a service that can -send an email every time someone pushes new commits. - -Because GitLab is open source we can ship with the code and tests for all -plugins. This allows the community to keep the plugins up to date so that they -always work in newer GitLab versions. - -For an overview of what projects services are available without logging in, -please see the [project_services directory][projects-code]. - -[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services - -Click on the service links to see -further configuration instructions and details. Contributions are welcome. - -## Services - -| Service | Description | -| ------- | ----------- | -| Asana | Asana - Teamwork without email | -| Assembla | Project Management Software (Source Commits Endpoint) | -| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | -| Buildkite | Continuous integration and deployments | -| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients | -| [Bugzilla](bugzilla.md) | Bugzilla issue tracker | -| Campfire | Simple web-based real-time group chat | -| Custom Issue Tracker | Custom issue tracker | -| Drone CI | Continuous Integration platform built on Docker, written in Go | -| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | -| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | -| Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | -| [HipChat](hipchat.md) | Private group chat and IM | -| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](jira.md) | JIRA issue tracker | -| JetBrains TeamCity CI | A continuous integration and build server | -| [Kubernetes](kubernetes.md) | A containerized deployment service | -| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | -| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | -| [Slack Notifications](slack.md) | Receive event notifications in Slack | -| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | -| PivotalTracker | Project Management Software (Source Commits Endpoint) | -| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | -| [Redmine](redmine.md) | Redmine issue tracker | - -## Services Templates - -Services templates is a way to set some predefined values in the Service of -your liking which will then be pre-filled on each project's Service. - -Read more about [Services Templates in this document](services_templates.md). +This document was moved to [user/project/integrations/project_services.md](../user/project/integrations/project_services.md). diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md index b9830ea7c38..6010aa4dc75 100644 --- a/doc/project_services/redmine.md +++ b/doc/project_services/redmine.md @@ -1,21 +1 @@ -# Redmine Service - -Go to your project's **Settings > Services > Redmine** and fill in the required -details as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | -| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | -| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | - -Once you have configured and enabled Redmine: - -- the **Issues** link on the GitLab project pages takes you to the appropriate - Redmine issue index -- clicking **New issue** on the project dashboard creates a new Redmine issue - -As an example, below is a configuration for a project named gitlab-ci. - -![Redmine configuration](img/redmine_configuration.png) +This document was moved to [user/project/integrations/redmine.md](../user/project/integrations/redmine.md). diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md index be6d13b6d2b..8905d667c5a 100644 --- a/doc/project_services/services_templates.md +++ b/doc/project_services/services_templates.md @@ -1,25 +1 @@ -# Services Templates - -A GitLab administrator can add a service template that sets a default for each -project. This makes it much easier to configure individual projects. - -After the template is created, the template details will be pre-filled on a -project's Service page. - -## Enable a Service template - -In GitLab's Admin area, navigate to **Service Templates** and choose the -service template you wish to create. - -For example, in the image below you can see Redmine. - -![Redmine service template](img/services_templates_redmine_example.png) - ---- - -**NOTE:** For each project, you will still need to configure the issue tracking -URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used -by your external issue tracker. Prior to GitLab v7.8, this ID was configured in -the project settings, and GitLab would automatically update the URL configured -in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs -must be configured directly within the project's **Services** settings. +This document was moved to [user/project/integrations/services_templates.md](../user/project/integrations/services_templates.md). diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index eaceb2be137..1d3f98705e3 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,50 +1 @@ -# Slack Notifications Service - -## On Slack - -To enable Slack integration you must create an incoming webhook integration on -Slack: - -1. [Sign in to Slack](https://slack.com/signin) -1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) -1. Choose the channel name you want to send notifications to. -1. Click **Add Incoming WebHooks Integration** -1. Copy the **Webhook URL**, we'll need this later for GitLab. - -## On GitLab - -After you set up Slack, it's time to set up GitLab. - -Go to your project's **Settings > Integrations > Slack Notifications** and you will see a -checkbox with the following events that can be triggered: - -- Push -- Issue -- Merge request -- Note -- Tag push -- Build -- 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 (`#`). - -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. | -| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | - -After you are all done, click **Save changes** for the changes to take effect. - ->**Note:** -You can set "branch,pushed,Compare changes" as highlight words on your Slack -profile settings, so that you can be aware of new commits when somebody pushes -them. - -![Slack configuration](img/slack_configuration.png) - -[slackhook]: https://my.slack.com/services/new/incoming-webhook +This document was moved to [user/project/integrations/slack.md](../user/project/integrations/slack.md). diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md index d9ff573d185..9554c8decc8 100644 --- a/doc/project_services/slack_slash_commands.md +++ b/doc/project_services/slack_slash_commands.md @@ -1,23 +1 @@ -# Slack slash commands - -> Introduced in GitLab 8.15 - -Slack commands give users an extra interface to perform common operations -from the chat environment. This allows one to, for example, create an issue as -soon as the idea was discussed in chat. -For all available commands try the help subcommand, for example: `/gitlab help`, -all review the [full list of commands](../integration/chat_commands.md). - -## Prerequisites - -A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. - -## Configuration - -First, navigate to the Slack Slash commands service page, found at your project's -**Settings** > **Services**, and you find the instructions there: - - ![Slack setup instructions](img/slack_setup.png) - -Once you've followed the instructions, mark the service as active and insert the token -you've received from Slack. After saving the service you are good to go! +This document was moved to [user/project/integrations/slack_slash_commands.md](../user/project/integrations/slack_slash_commands.md). diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index bb46aebf4b5..faabc53ce72 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -2,7 +2,7 @@ If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. -With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. +With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. @@ -10,4 +10,4 @@ Because Webhook requests are made by the GitLab server itself, these have comple If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". -To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
\ No newline at end of file +To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 9803937fcf9..9e391d647a8 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -4,10 +4,12 @@ Git is a distributed version control system, which means you can work locally but you can also share or "push" your changes to other servers. Before you can push your changes to a GitLab server you need a secure communication channel for sharing information. -GitLab uses Public-key or asymmetric cryptography -which encrypts a communication channel by locking it with your "private key" -and allows trusted parties to unlock it with your "public key". -If someone does not have your public key they cannot access the unencrypted message. + +The SSH protocol provides this security and allows you to authenticate to the +GitLab remote server without supplying your username or password each time. + +For a more detailed explanation of how the SSH protocol works, we advise you to +read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process). ## Locating an existing SSH key pair diff --git a/doc/university/README.md b/doc/university/README.md index 12727e9d56f..c798e0d760d 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -189,10 +189,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 3.9. Integrations 1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415) -1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html) +1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ce/user/project/integrations/jira.html) 1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html) -1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md) -1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md) +1. [How to Integrate Bamboo with GitLab](https://docs.gitlab.com/ce/user/project/integrations/bamboo.html) +1. [How to Integrate Slack with GitLab](https://docs.gitlab.com/ce/user/project/integrations/slack.html) 1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) 1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/) diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 20e7ea1987f..979a1c5d310 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -573,7 +573,7 @@ A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building softwa ### Webhooks -A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient. +A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integrations/webhooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient. ### Wiki diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md new file mode 100644 index 00000000000..51668128c62 --- /dev/null +++ b/doc/user/project/integrations/bamboo.md @@ -0,0 +1,60 @@ +# Atlassian Bamboo CI Service + +GitLab provides integration with Atlassian Bamboo for continuous integration. +When configured, pushes to a project will trigger a build in Bamboo automatically. +Merge requests will also display CI status showing whether the build is pending, +failed, or completed successfully. It also provides a link to the Bamboo build +page for more information. + +Bamboo doesn't quite provide the same features as a traditional build system when +it comes to accepting webhooks and commit data. There are a few things that +need to be configured in a Bamboo build plan before GitLab can integrate. + +## Setup + +### Complete these steps in Bamboo: + +1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions' +dropdown. +1. Select the 'Triggers' tab. +1. Click 'Add trigger'. +1. Enter a description such as 'GitLab trigger' +1. Choose 'Repository triggers the build when changes are committed' +1. Check one or more repositories checkboxes +1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a +whitelist of IP addresses that are allowed to trigger Bamboo builds. +1. Save the trigger. +1. In the left pane, select a build stage. If you have multiple build stages +you want to select the last stage that contains the git checkout task. +1. Select the 'Miscellaneous' tab. +1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}' +in the 'Labels' box. +1. Save + +Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo +service in GitLab + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure to trigger builds. +1. Select 'Settings' in the top navigation. +1. Select 'Services' in the left navigation. +1. Click 'Atlassian Bamboo CI' +1. Select the 'Active' checkbox. +1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' +1. Enter the build key from your Bamboo build plan. Build keys are a short, +all capital letter, identifier that is unique. It will be something like PR-BLD +1. If necessary, enter username and password for a Bamboo user that has +access to trigger the build plan. Leave these fields blank if you do not require +authentication. +1. Save or optionally click 'Test Settings'. Please note that 'Test Settings' +will actually trigger a build in Bamboo. + +## Troubleshooting + +If builds are not triggered, these are a couple of things to keep in mind. + +1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger +IP addresses'. +1. Remember that GitLab only triggers builds on push events. A commit via the +web interface will not trigger CI currently. diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md new file mode 100644 index 00000000000..215ed6fe9cc --- /dev/null +++ b/doc/user/project/integrations/bugzilla.md @@ -0,0 +1,17 @@ +# Bugzilla Service + +Go to your project's **Settings > Services > Bugzilla** and fill in the required +details as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | +| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | +| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | + +Once you have configured and enabled Bugzilla: + +- the **Issues** link on the GitLab project pages takes you to the appropriate + Bugzilla product page +- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md new file mode 100644 index 00000000000..af0b1a287c7 --- /dev/null +++ b/doc/user/project/integrations/builds_emails.md @@ -0,0 +1,16 @@ +## Enabling build emails + +To receive e-mail notifications about the result status of your builds, visit +your project's **Settings > Services > Builds emails** and activate the service. + +In the _Recipients_ area, provide a list of e-mails separated by comma. + +Check the _Add pusher_ checkbox if you want the committer to also receive +e-mail notifications about each build's status. + +If you enable the _Notify only broken builds_ option, e-mail notifications will +be sent only for failed builds. + +--- + +![Builds emails service settings](img/builds_emails_service.png) diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md new file mode 100644 index 00000000000..2f9f36f962e --- /dev/null +++ b/doc/user/project/integrations/emails_on_push.md @@ -0,0 +1,17 @@ +## Enabling emails on push + +To receive email notifications for every change that is pushed to the project, visit +your project's **Settings > Services > Emails on push** and activate the service. + +In the _Recipients_ area, provide a list of emails separated by commas. + +You can configure any of the following settings depending on your preference. + ++ **Push events** - Email will be triggered when a push event is recieved ++ **Tag push events** - Email will be triggered when a tag is created and pushed ++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). ++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + +--- + +![Email on push service settings](img/emails_on_push_service.png) diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md new file mode 100644 index 00000000000..021a93a288f --- /dev/null +++ b/doc/user/project/integrations/hipchat.md @@ -0,0 +1,54 @@ +# Atlassian HipChat + +GitLab provides a way to send HipChat notifications upon a number of events, +such as when a user pushes code, creates a branch or tag, adds a comment, and +creates a merge request. + +## Setup + +GitLab requires the use of a HipChat v2 API token to work. v1 tokens are +not supported at this time. Note the differences between v1 and v2 tokens: + +HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 +token is allowed to send messages to *any* room. + +HipChat v2 API has tokens that are can be created using the Integrations tab +in the Group or Room admin page. By design, these are lightweight tokens that +allow GitLab to send messages only to *one* room. + +### Complete these steps in HipChat: + +1. Go to: https://admin.hipchat.com/admin +1. Click on "Group Admin" -> "Integrations". +1. Find "Build Your Own!" and click "Create". +1. Select the desired room, name the integration "GitLab", and click "Create". +1. In the "Send messages to this room by posting this URL" column, you should +see a URL in the format: + +``` + https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> +``` + +HipChat is now ready to accept messages from GitLab. Next, set up the HipChat +service in GitLab. + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "HipChat". +1. Select the "Active" checkbox. +1. Insert the `token` field from the URL into the `Token` field on the Web page. +1. Insert the `room` field from the URL into the `Room` field on the Web page. +1. Save or optionally click "Test Settings". + +## Troubleshooting + +If you do not see notifications, make sure you are using a HipChat v2 API +token, not a v1 token. + +Note that the v2 token is tied to a specific room. If you want to be able to +specify arbitrary rooms, you can create an API token for a specific user in +HipChat under "Account settings" and "API access". Use the `XXX` value under +`auth_token=XXX`. diff --git a/doc/project_services/img/builds_emails_service.png b/doc/user/project/integrations/img/builds_emails_service.png Binary files differindex 9dbbed03833..9dbbed03833 100644 --- a/doc/project_services/img/builds_emails_service.png +++ b/doc/user/project/integrations/img/builds_emails_service.png diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png Binary files differindex df301aa1eeb..df301aa1eeb 100644 --- a/doc/project_services/img/emails_on_push_service.png +++ b/doc/user/project/integrations/img/emails_on_push_service.png diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png Binary files differindex 27dac49260c..27dac49260c 100644 --- a/doc/project_services/img/jira_add_user_to_group.png +++ b/doc/user/project/integrations/img/jira_add_user_to_group.png diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png Binary files differindex 06c4e84fc61..06c4e84fc61 100644 --- a/doc/project_services/img/jira_create_new_group.png +++ b/doc/user/project/integrations/img/jira_create_new_group.png diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/user/project/integrations/img/jira_create_new_group_name.png Binary files differindex bfc0dc6b2e9..bfc0dc6b2e9 100644 --- a/doc/project_services/img/jira_create_new_group_name.png +++ b/doc/user/project/integrations/img/jira_create_new_group_name.png diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png Binary files differindex e9c03ed770d..e9c03ed770d 100644 --- a/doc/project_services/img/jira_create_new_user.png +++ b/doc/user/project/integrations/img/jira_create_new_user.png diff --git a/doc/project_services/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png Binary files differindex 9d64cc57269..9d64cc57269 100644 --- a/doc/project_services/img/jira_group_access.png +++ b/doc/user/project/integrations/img/jira_group_access.png diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/user/project/integrations/img/jira_issue_reference.png Binary files differindex 72c81460df7..72c81460df7 100644 --- a/doc/project_services/img/jira_issue_reference.png +++ b/doc/user/project/integrations/img/jira_issue_reference.png diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/user/project/integrations/img/jira_merge_request_close.png Binary files differindex 0f82ceba557..0f82ceba557 100644 --- a/doc/project_services/img/jira_merge_request_close.png +++ b/doc/user/project/integrations/img/jira_merge_request_close.png diff --git a/doc/project_services/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png Binary files differindex 8540a427461..8540a427461 100644 --- a/doc/project_services/img/jira_project_name.png +++ b/doc/user/project/integrations/img/jira_project_name.png diff --git a/doc/project_services/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png Binary files differindex 8e073b84ff9..8e073b84ff9 100644 --- a/doc/project_services/img/jira_service.png +++ b/doc/user/project/integrations/img/jira_service.png diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/user/project/integrations/img/jira_service_close_comment.png Binary files differindex bb9cd7e3d13..bb9cd7e3d13 100644 --- a/doc/project_services/img/jira_service_close_comment.png +++ b/doc/user/project/integrations/img/jira_service_close_comment.png diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/user/project/integrations/img/jira_service_close_issue.png Binary files differindex c85b1d1dd97..c85b1d1dd97 100644 --- a/doc/project_services/img/jira_service_close_issue.png +++ b/doc/user/project/integrations/img/jira_service_close_issue.png diff --git a/doc/project_services/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png Binary files differindex c74351b57b8..c74351b57b8 100644 --- a/doc/project_services/img/jira_service_page.png +++ b/doc/user/project/integrations/img/jira_service_page.png diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png Binary files differindex f81c5b5fc87..f81c5b5fc87 100644 --- a/doc/project_services/img/jira_user_management_link.png +++ b/doc/user/project/integrations/img/jira_user_management_link.png diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png Binary files differindex e62fb202613..e62fb202613 100644 --- a/doc/project_services/img/jira_workflow_screenshot.png +++ b/doc/user/project/integrations/img/jira_workflow_screenshot.png diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png Binary files differindex 349a2dc8456..349a2dc8456 100644 --- a/doc/project_services/img/kubernetes_configuration.png +++ b/doc/user/project/integrations/img/kubernetes_configuration.png diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/user/project/integrations/img/mattermost_add_slash_command.png Binary files differindex 7759efa183c..7759efa183c 100644 --- a/doc/project_services/img/mattermost_add_slash_command.png +++ b/doc/user/project/integrations/img/mattermost_add_slash_command.png diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/user/project/integrations/img/mattermost_bot_auth.png Binary files differindex 830b7849f3d..830b7849f3d 100644 --- a/doc/project_services/img/mattermost_bot_auth.png +++ b/doc/user/project/integrations/img/mattermost_bot_auth.png diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/user/project/integrations/img/mattermost_bot_available_commands.png Binary files differindex b51798cf10d..b51798cf10d 100644 --- a/doc/project_services/img/mattermost_bot_available_commands.png +++ b/doc/user/project/integrations/img/mattermost_bot_available_commands.png diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png Binary files differindex a62e4b792f9..a62e4b792f9 100644 --- a/doc/project_services/img/mattermost_config_help.png +++ b/doc/user/project/integrations/img/mattermost_config_help.png diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png Binary files differindex 3c5ff5ee317..3c5ff5ee317 100644 --- a/doc/project_services/img/mattermost_configuration.png +++ b/doc/user/project/integrations/img/mattermost_configuration.png diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/user/project/integrations/img/mattermost_console_integrations.png Binary files differindex 92a30da5be0..92a30da5be0 100644 --- a/doc/project_services/img/mattermost_console_integrations.png +++ b/doc/user/project/integrations/img/mattermost_console_integrations.png diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/user/project/integrations/img/mattermost_gitlab_token.png Binary files differindex 257018914d2..257018914d2 100644 --- a/doc/project_services/img/mattermost_gitlab_token.png +++ b/doc/user/project/integrations/img/mattermost_gitlab_token.png diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/user/project/integrations/img/mattermost_goto_console.png Binary files differindex 3354c2a24b4..3354c2a24b4 100644 --- a/doc/project_services/img/mattermost_goto_console.png +++ b/doc/user/project/integrations/img/mattermost_goto_console.png diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png Binary files differindex 12766ab2b34..12766ab2b34 100644 --- a/doc/project_services/img/mattermost_slash_command_configuration.png +++ b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/user/project/integrations/img/mattermost_slash_command_token.png Binary files differindex c38f37c203c..c38f37c203c 100644 --- a/doc/project_services/img/mattermost_slash_command_token.png +++ b/doc/user/project/integrations/img/mattermost_slash_command_token.png diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/user/project/integrations/img/mattermost_team_integrations.png Binary files differindex 69d4a231e5a..69d4a231e5a 100644 --- a/doc/project_services/img/mattermost_team_integrations.png +++ b/doc/user/project/integrations/img/mattermost_team_integrations.png diff --git a/doc/project_services/img/redmine_configuration.png b/doc/user/project/integrations/img/redmine_configuration.png Binary files differindex 7b6dd271401..7b6dd271401 100644 --- a/doc/project_services/img/redmine_configuration.png +++ b/doc/user/project/integrations/img/redmine_configuration.png diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png Binary files differindex 50d20510daf..50d20510daf 100644 --- a/doc/project_services/img/services_templates_redmine_example.png +++ b/doc/user/project/integrations/img/services_templates_redmine_example.png diff --git a/doc/project_services/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png Binary files differindex fc8e58e686b..fc8e58e686b 100644 --- a/doc/project_services/img/slack_configuration.png +++ b/doc/user/project/integrations/img/slack_configuration.png diff --git a/doc/project_services/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png Binary files differindex f69817f2b78..f69817f2b78 100644 --- a/doc/project_services/img/slack_setup.png +++ b/doc/user/project/integrations/img/slack_setup.png diff --git a/doc/web_hooks/ssl.png b/doc/user/project/integrations/img/webhooks_ssl.png Binary files differindex 21ddec4ebdf..21ddec4ebdf 100644 --- a/doc/web_hooks/ssl.png +++ b/doc/user/project/integrations/img/webhooks_ssl.png diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md new file mode 100644 index 00000000000..766ffb1f65c --- /dev/null +++ b/doc/user/project/integrations/index.md @@ -0,0 +1,18 @@ +# Project integrations + +## Project services + +Project services allow you to integrate GitLab with other applications. +They are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. + +[Learn more about project services.](project_services.md) + +## Webhooks + +Project webhooks allow you to trigger a URL if for example new code is pushed or +a new issue is created. You can configure webhooks to listen for specific events +like pushes, issues or merge requests. GitLab will send a POST request with data +to the webhook URL. + +[Learn more about webhooks.](webhooks.md) diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md new file mode 100644 index 00000000000..25c0c3ad2a6 --- /dev/null +++ b/doc/user/project/integrations/irker.md @@ -0,0 +1,51 @@ +# Irker IRC Gateway + +GitLab provides a way to push update messages to an Irker server. When +configured, pushes to a project will trigger the service to send data directly +to the Irker server. + +See the project homepage for further info: https://gitlab.com/esr/irker + +## Needed setup + +You will first need an Irker daemon. You can download the Irker code from its +repository on https://gitlab.com/esr/irker: + +``` +git clone https://gitlab.com/esr/irker.git +``` + +Once you have downloaded the code, you can run the python script named `irkerd`. +This script is the gateway script, it acts both as an IRC client, for sending +messages to an IRC server obviously, and as a TCP server, for receiving messages +from the GitLab service. + +If the Irker server runs on the same machine, you are done. If not, you will +need to follow the firsts steps of the next section. + +## Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "Irker". +1. Select the "Active" checkbox. +1. Enter the server host address where `irkerd` runs (defaults to `localhost`) +in the `Server host` field on the Web page +1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the +`Server port` field on the Web page. +1. Optional: if `Default IRC URI` is set, it has to be in the format +`irc[s]://domain.name` and will be prepend to each and every channel provided +by the user which is not a full URI. +1. Specify the recipients (e.g. #channel1, user1, etc.) +1. Save or optionally click "Test Settings". + +## Note on Irker recipients + +Irker accepts channel names of the form `chan` and `#chan`, both for the +`#chan` channel. If you want to send messages in query, you will need to add +`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter +case, `Aorimn` is treated as a nick and no more as a channel name. + +Irker can also join password-protected channels. Users need to append +`?key=thesecretpassword` to the chan name. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md new file mode 100644 index 00000000000..233a2583c36 --- /dev/null +++ b/doc/user/project/integrations/jira.md @@ -0,0 +1,208 @@ +# GitLab JIRA integration + +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Configuration + +In order to enable the JIRA service in GitLab, you need to first configure the +project in JIRA and then enter the correct values in GitLab. + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access ➔ View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +>**Notes:** +- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or + higher is required. +- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified + the configuration options you have to enter. If you are using an older version, + [follow this documentation][jira-repo-old-docs]. + +To enable JIRA integration in a project, navigate to your project's +**Services ➔ JIRA** and fill in the required details on the page as described +in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | +| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | +| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +## JIRA issues + +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` +USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: +ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). + +There are currently three trigger words, and you can use either one to achieve +the same goal: + +- `Resolves PROJECT-1` +- `Closes PROJECT-1` +- `Fixes PROJECT-1` + +where `PROJECT-1` is the issue ID of the JIRA project. + +### JIRA issue closing example + +Let's consider the following example: + +1. For the project named `PROJECT` in JIRA, we implemented a new feature + and created a merge request in GitLab. +1. This feature was requested in JIRA issue `PROJECT-7` and the merge request + in GitLab contains the improvement +1. In the merge request description we use the issue closing trigger + `Closes PROJECT-7`. +1. Once the merge request is merged, the JIRA issue will be automatically closed + with a comment and an associated link to the commit that resolved the issue. + +--- + +In the following screenshot you can see what the link references to the JIRA +issue look like. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) + +--- + +![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) + +## Troubleshooting + +If things don't work as expected that's usually because you have configured +incorrectly the JIRA-GitLab integration. + +### GitLab is unable to comment on a ticket + +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition +the ticket, if you'd like GitLab to also take care of closing them. +JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. + +### GitLab is unable to close a ticket + +Make sure the `Transition ID` you set within the JIRA settings matches the one +your project needs to close a ticket. + +[services-templates]: services_templates.md +[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md new file mode 100644 index 00000000000..99aa9e44bdb --- /dev/null +++ b/doc/user/project/integrations/kubernetes.md @@ -0,0 +1,63 @@ +# GitLab Kubernetes / OpenShift integration + +GitLab can be configured to interact with Kubernetes, or other systems using the +Kubernetes API (such as OpenShift). + +Each project can be configured to connect to a different Kubernetes cluster, see +the [configuration](#configuration) section. + +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see the [Services Templates](services_templates.md) document. + +## Configuration + +![Kubernetes configuration settings](img/kubernetes_configuration.png) + +The Kubernetes service takes the following arguments: + +1. Kubernetes namespace +1. API URL +1. Service token +1. Custom CA bundle + +The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes +exposes several APIs - we want the "base" URL that is common to all of them, +e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + +GitLab authenticates against Kubernetes using service tokens, which are +scoped to a particular `namespace`. If you don't have a service token yet, +you can follow the +[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) +to create one. You can also view or create service tokens in the +[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit +`Config -> Secrets`. + +Fill in the service token and namespace according to the values you just got. +If the API is using a self-signed TLS certificate, you'll also need to include +the `ca.crt` contents as the `Custom CA bundle`. + +## Deployment variables + +The Kubernetes service exposes following +[deployment variables](../ci/variables/README.md#deployment-variables) in the +GitLab CI build environment: + +- `KUBE_URL` - equal to the API URL +- `KUBE_TOKEN` +- `KUBE_NAMESPACE` +- `KUBE_CA_PEM` - only if a custom CA bundle was specified + +## Web terminals + +>**NOTE:** +Added in GitLab 8.15. You must be the project owner or have `master` permissions +to use terminals. Support is currently limited to the first container in the +first pod of your environment. + +When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +support to your environments. This is based on the `exec` functionality found in +Docker and Kubernetes, so you get a new shell session within your existing +containers. To use this integration, you should deploy to Kubernetes using +the deployment variables above, ensuring any pods you create are labelled with +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md new file mode 100644 index 00000000000..fbc7dfeee6d --- /dev/null +++ b/doc/user/project/integrations/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- 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. + +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... | +| **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. | + + +![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md new file mode 100644 index 00000000000..67cb88104c1 --- /dev/null +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -0,0 +1,163 @@ +# Mattermost slash commands + +> Introduced in GitLab 8.14 + +Mattermost commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in Mattermost. + +## Prerequisites + +Mattermost 3.4 and up is required. + +If you have the Omnibus GitLab package installed, Mattermost is already bundled +in it. All you have to do is configure it. Read more in the +[Omnibus GitLab Mattermost documentation][omnimmdocs]. + +## Automated Configuration + +If Mattermost is installed on the same server as GitLab, the configuration process can be +done for you by GitLab. + +Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. + +## Manual Configuration + +The configuration consists of two parts. First you need to enable the slash +commands in Mattermost and then enable the service in GitLab. + +### Step 1. Enable custom slash commands in Mattermost + +This step is only required when using a source install, omnibus installs will be +preconfigured with the right settings. + +The first thing to do in Mattermost is to enable custom slash commands from +the administrator console. + +1. Log in with an account that has admin privileges and navigate to the system + console. + + ![Mattermost go to console](img/mattermost_goto_console.png) + + --- + +1. Click **Custom integrations** and set **Enable Custom Slash Commands**, + **Enable custom integrations to override usernames**, and **Override + custom integrations to override profile picture icons** to true + + ![Mattermost console](img/mattermost_console_integrations.png) + + --- + +1. Click **Save** at the bottom to save the changes. + +### Step 2. Open the Mattermost slash commands service in GitLab + +1. Open a new tab for GitLab and go to your project's settings + **Services ➔ Mattermost command**. A screen will appear with all the values you + need to copy in Mattermost as described in the next step. Leave the window open. + + >**Note:** + GitLab will propose some values for the Mattermost settings. The only one + required to copy-paste as-is is the **Request URL**, all the others are just + suggestions. + + ![Mattermost setup instructions](img/mattermost_config_help.png) + + --- + +1. Proceed to the next step and create a slash command in Mattermost with the + above values. + +### Step 3. Create a new custom slash command in Mattermost + +Now that you have enabled custom slash commands in Mattermost and opened +the Mattermost slash commands service in GitLab, it's time to copy these values +in a new slash command. + +1. Back to Mattermost, under your team page settings, you should see the + **Integrations** option. + + ![Mattermost team integrations](img/mattermost_team_integrations.png) + + --- + +1. Go to the **Slash Commands** integration and add a new one by clicking the + **Add Slash Command** button. + + ![Mattermost add command](img/mattermost_add_slash_command.png) + + --- + +1. Fill in the options for the custom command as described in + [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab). + + >**Note:** + If you plan on connecting multiple projects, pick a slash command trigger + word that relates to your projects such as `/gitlab-project-name` or even + just `/project-name`. Only use `/gitlab` if you will only connect a single + project to your Mattermost team. + + ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png) + +1. After you setup all the values, copy the token (we will use it below) and + click **Done**. + + ![Mattermost slash command token](img/mattermost_slash_command_token.png) + +### Step 4. Copy the Mattermost token into the Mattermost slash command service + +1. In GitLab, paste the Mattermost token you copied in the previous step and + check the **Active** checkbox. + + ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png) + +1. Click **Save changes** for the changes to take effect. + +--- + +You are now set to start using slash commands in Mattermost that talk to the +GitLab project you configured. + +## Authorizing Mattermost to interact with GitLab + +The first time a user will interact with the newly created slash commands, +Mattermost will trigger an authorization process. + +![Mattermost bot authorize](img/mattermost_bot_auth.png) + +This will connect your Mattermost user with your GitLab user. You can +see all authorized chat accounts in your profile's page under **Chat**. + +When the authorization process is complete, you can start interacting with +GitLab using the Mattermost commands. + +## Available slash commands + +The available slash commands are: + +| Command | Description | Example | +| ------- | ----------- | ------- | +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | + +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> + +![Mattermost bot available commands](img/mattermost_bot_available_commands.png) + +## Permissions + +The permissions to run the [available commands](#available-commands) derive from +the [permissions you have on the project](../user/permissions.md#project). + +## Further reading + +- [Mattermost slash commands documentation][mmslashdocs] +- [Omnibus GitLab Mattermost][omnimmdocs] + + +[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ +[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html +[ciyaml]: ../ci/yaml/README.md diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md new file mode 100644 index 00000000000..547d855d777 --- /dev/null +++ b/doc/user/project/integrations/project_services.md @@ -0,0 +1,59 @@ +# Project Services + +Project services allow you to integrate GitLab with other applications. Below +is list of the currently supported ones. + +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. + +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. + +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. + +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +Click on the service links to see +further configuration instructions and details. Contributions are welcome. + +## Services + +| Service | Description | +| ------- | ----------- | +| Asana | Asana - Teamwork without email | +| Assembla | Project Management Software (Source Commits Endpoint) | +| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | +| Buildkite | Continuous integration and deployments | +| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients | +| [Bugzilla](bugzilla.md) | Bugzilla issue tracker | +| Campfire | Simple web-based real-time group chat | +| Custom Issue Tracker | Custom issue tracker | +| Drone CI | Continuous Integration platform built on Docker, written in Go | +| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | +| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | +| Flowdock | Flowdock is a collaboration web app for technical teams | +| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| [HipChat](hipchat.md) | Private group chat and IM | +| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | +| [JIRA](jira.md) | JIRA issue tracker | +| JetBrains TeamCity CI | A continuous integration and build server | +| [Kubernetes](kubernetes.md) | A containerized deployment service | +| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | +| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | +| PivotalTracker | Project Management Software (Source Commits Endpoint) | +| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | +| [Redmine](redmine.md) | Redmine issue tracker | + +## Services Templates + +Services templates is a way to set some predefined values in the Service of +your liking which will then be pre-filled on each project's Service. + +Read more about [Services Templates in this document](services_templates.md). diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md new file mode 100644 index 00000000000..b9830ea7c38 --- /dev/null +++ b/doc/user/project/integrations/redmine.md @@ -0,0 +1,21 @@ +# Redmine Service + +Go to your project's **Settings > Services > Redmine** and fill in the required +details as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | +| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | +| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | + +Once you have configured and enabled Redmine: + +- the **Issues** link on the GitLab project pages takes you to the appropriate + Redmine issue index +- clicking **New issue** on the project dashboard creates a new Redmine issue + +As an example, below is a configuration for a project named gitlab-ci. + +![Redmine configuration](img/redmine_configuration.png) diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md new file mode 100644 index 00000000000..be6d13b6d2b --- /dev/null +++ b/doc/user/project/integrations/services_templates.md @@ -0,0 +1,25 @@ +# Services Templates + +A GitLab administrator can add a service template that sets a default for each +project. This makes it much easier to configure individual projects. + +After the template is created, the template details will be pre-filled on a +project's Service page. + +## Enable a Service template + +In GitLab's Admin area, navigate to **Service Templates** and choose the +service template you wish to create. + +For example, in the image below you can see Redmine. + +![Redmine service template](img/services_templates_redmine_example.png) + +--- + +**NOTE:** For each project, you will still need to configure the issue tracking +URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used +by your external issue tracker. Prior to GitLab v7.8, this ID was configured in +the project settings, and GitLab would automatically update the URL configured +in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs +must be configured directly within the project's **Services** settings. diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md new file mode 100644 index 00000000000..eaceb2be137 --- /dev/null +++ b/doc/user/project/integrations/slack.md @@ -0,0 +1,50 @@ +# Slack Notifications Service + +## On Slack + +To enable Slack integration you must create an incoming webhook integration on +Slack: + +1. [Sign in to Slack](https://slack.com/signin) +1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) +1. Choose the channel name you want to send notifications to. +1. Click **Add Incoming WebHooks Integration** +1. Copy the **Webhook URL**, we'll need this later for GitLab. + +## On GitLab + +After you set up Slack, it's time to set up GitLab. + +Go to your project's **Settings > Integrations > Slack Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- 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 (`#`). + +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. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + +After you are all done, click **Save changes** for the changes to take effect. + +>**Note:** +You can set "branch,pushed,Compare changes" as highlight words on your Slack +profile settings, so that you can be aware of new commits when somebody pushes +them. + +![Slack configuration](img/slack_configuration.png) + +[slackhook]: https://my.slack.com/services/new/incoming-webhook diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md new file mode 100644 index 00000000000..d9ff573d185 --- /dev/null +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -0,0 +1,23 @@ +# Slack slash commands + +> Introduced in GitLab 8.15 + +Slack commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in chat. +For all available commands try the help subcommand, for example: `/gitlab help`, +all review the [full list of commands](../integration/chat_commands.md). + +## Prerequisites + +A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. + +## Configuration + +First, navigate to the Slack Slash commands service page, found at your project's +**Settings** > **Services**, and you find the instructions there: + + ![Slack setup instructions](img/slack_setup.png) + +Once you've followed the instructions, mark the service as active and insert the token +you've received from Slack. After saving the service you are good to go! diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md new file mode 100644 index 00000000000..9d775355c4c --- /dev/null +++ b/doc/user/project/integrations/webhooks.md @@ -0,0 +1,1025 @@ +# Webhooks + +>**Note:** +Starting from GitLab 8.5: +- the `repository` key is deprecated in favor of the `project` key +- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key +- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key + +Project webhooks allow you to trigger a URL if for example new code is pushed or +a new issue is created. You can configure webhooks to listen for specific events +like pushes, issues or merge requests. GitLab will send a POST request with data +to the webhook URL. + +Webhooks can be used to update an external issue tracker, trigger CI builds, +update a backup mirror, or even deploy to your production server. + +Navigate to the webhooks page by choosing **Webhooks** from your project's +settings which can be found under the wheel icon in the upper right corner. + +## Webhook endpoint tips + +If you are writing your own endpoint (web server) that will receive +GitLab webhooks keep in mind the following things: + +- Your endpoint should send its HTTP response as fast as possible. If + you wait too long, GitLab may decide the hook failed and retry it. +- Your endpoint should ALWAYS return a valid HTTP response. If you do + not do this then GitLab will think the hook failed and retry it. + Most HTTP libraries take care of this for you automatically but if + you are writing a low-level hook this is important to remember. +- GitLab ignores the HTTP status code returned by your endpoint. + +## Secret token + +If you specify a secret token, it will be sent with the hook request in the +`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify +that the request is legitimate. + +## SSL verification + +By default, the SSL certificate of the webhook endpoint is verified based on +an internal list of Certificate Authorities, which means the certificate cannot +be self-signed. + +You can turn this off in the webhook settings in your GitLab projects. + +![SSL Verification](img/webhooks_ssl.png) + +## Events + +Below are described the supported events. + +### Push events + +Triggered when you push to the repository except when pushing tags. + +**Request header**: + +``` +X-Gitlab-Event: Push Hook +``` + +**Request body:** + +```json +{ + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} +``` + +### Tag events + +Triggered when you create (or delete) tags to the repository. + +**Request header**: + +``` +X-Gitlab-Event: Tag Push Hook +``` + +**Request body:** + +```json +{ + "object_kind": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 1, + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ + "name": "Example", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +``` + +### Issues events + +Triggered when a new issue is created or an existing issue was updated/closed/reopened. + +**Request header**: + +``` +X-Gitlab-Event: Issue Hook +``` + +**Request body:** + +```json +{ + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": null, + "description": "Create new API for manipulations with repository", + "milestone_id": null, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } +} +``` +### Comment events + +Triggered when a new comment is made on commits, merge requests, issues, and code snippets. +The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The +payload will also include information about the target of the comment. For example, +a comment on a issue will include the specific issue information under the `issue` key. +Valid target types: + +1. `commit` +2. `merge_request` +3. `issue` +4. `snippet` + +#### Comment on commit + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +} +``` + +#### Comment on merge request + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "locked_at": null, + "source":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "target": { + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21: 00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + }, + "work_in_progress": false, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +} +``` + +#### Comment on issue + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"diaspora", + "url":"git@example.com:mike/diaspora.git", + "description":"", + "homepage":"http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +} +``` + +#### Comment on code snippet + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"Gitlab Test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "description":"Aut reprehenderit ut est.", + "homepage":"http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-09 02:40:38 UTC", + "updated_at": "2015-04-09 02:40:38 UTC", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +} +``` + +### Merge request events + +Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: Merge Request Hook +``` + +**Request body:** + +```json +{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "object_attributes": { + "id": 99, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2013-12-03T17:23:34Z", + "updated_at": "2013-12-03T17:23:34Z", + "st_commits": null, + "st_diffs": null, + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +} +``` + +### Wiki Page events + +Triggered when a wiki page is created or edited. + +**Request Header**: + +``` +X-Gitlab-Event: Wiki Page Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "wiki_page", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project": { + "name": "awesome-project", + "description": "This is awesome", + "web_url": "http://example.com/root/awesome-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:root/awesome-project.git", + "git_http_url": "http://example.com/root/awesome-project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "root/awesome-project", + "default_branch": "master", + "homepage": "http://example.com/root/awesome-project", + "url": "git@example.com:root/awesome-project.git", + "ssh_url": "git@example.com:root/awesome-project.git", + "http_url": "http://example.com/root/awesome-project.git" + }, + "wiki": { + "web_url": "http://example.com/root/awesome-project/wikis/home", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", + "default_branch": "master" + }, + "object_attributes": { + "title": "Awesome", + "content": "awesome content goes here", + "format": "markdown", + "message": "adding an awesome page to the wiki", + "slug": "awesome", + "url": "http://example.com/root/awesome-project/wikis/awesome", + "action": "create" + } +} +``` + +### Pipeline events + +Triggered on status change of Pipeline. + +**Request Header**: + +``` +X-Gitlab-Event: Pipeline Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "pipeline", + "object_attributes":{ + "id": 31, + "ref": "master", + "tag": false, + "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "status": "success", + "stages":[ + "build", + "test", + "deploy" + ], + "created_at": "2016-08-12 15:23:28 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "duration": 63 + }, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "project":{ + "name": "Gitlab Test", + "description": "Atque in sunt eos similique dolores voluptatem.", + "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "avatar_url": null, + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 20, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master" + }, + "commit":{ + "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "message": "test\n", + "timestamp": "2016-08-12T17:23:21+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "author":{ + "name": "User", + "email": "user@gitlab.com" + } + }, + "builds":[ + { + "id": 380, + "stage": "deploy", + "name": "production", + "status": "skipped", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "manual", + "manual": true, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 377, + "stage": "test", + "name": "test-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 378, + "stage": "test", + "name": "test-build", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 376, + "stage": "build", + "name": "build-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:24:56 UTC", + "finished_at": "2016-08-12 15:25:26 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 379, + "stage": "deploy", + "name": "staging", + "status": "created", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + } + ] +} +``` + +### Build events + +Triggered on status change of a Build. + +**Request Header**: + +``` +X-Gitlab-Event: Build Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "build", + "ref": "gitlab-script-trigger", + "tag": false, + "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "project_id": 380, + "project_name": "gitlab-org/gitlab-test", + "user": { + "id": 3, + "name": "User", + "email": "user@gitlab.com" + }, + "commit": { + "id": 2366, + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "message": "test\n", + "author_name": "User", + "author_email": "user@gitlab.com", + "status": "created", + "duration": null, + "started_at": null, + "finished_at": null + }, + "repository": { + "name": "gitlab_test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "description": "Atque in sunt eos similique dolores voluptatem.", + "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "visibility_level": 20 + } +} +``` + +## Example webhook receiver + +If you want to see GitLab's webhooks in action for testing purposes you can use +a simple echo script running in a console session. For the following script to +work you need to have Ruby installed. + +Save the following file as `print_http_body.rb`: + +```ruby +require 'webrick' + +server = WEBrick::HTTPServer.new(:Port => ARGV.first) +server.mount_proc '/' do |req, res| + puts req.body +end + +trap 'INT' do + server.shutdown +end +server.start +``` + +Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb +8000`. Then add your server as a webhook receiver in GitLab as +`http://my.host:8000/`. + +When you press 'Test Hook' in GitLab, you should see something like this in the +console: + +``` +{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} +example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 +- -> / +``` diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index a6546cffce2..2fddd7c6503 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -32,5 +32,6 @@ do. | `/wip` | Toggle the Work In Progress status | | <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | | `/remove_estimate` | Remove estimated time | -| <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time | +| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | | `/remove_time_spent` | Remove time spent | +| `/target_branch <Branch Name>` | Set target branch for current merge request | diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 1659dd1f6cb..0ebe5eea173 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,1025 +1 @@ -# Webhooks - ->**Note:** -Starting from GitLab 8.5: -- the `repository` key is deprecated in favor of the `project` key -- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key -- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key - -Project webhooks allow you to trigger a URL if for example new code is pushed or -a new issue is created. You can configure webhooks to listen for specific events -like pushes, issues or merge requests. GitLab will send a POST request with data -to the webhook URL. - -Webhooks can be used to update an external issue tracker, trigger CI builds, -update a backup mirror, or even deploy to your production server. - -Navigate to the webhooks page by choosing **Webhooks** from your project's -settings which can be found under the wheel icon in the upper right corner. - -## Webhook endpoint tips - -If you are writing your own endpoint (web server) that will receive -GitLab webhooks keep in mind the following things: - -- Your endpoint should send its HTTP response as fast as possible. If - you wait too long, GitLab may decide the hook failed and retry it. -- Your endpoint should ALWAYS return a valid HTTP response. If you do - not do this then GitLab will think the hook failed and retry it. - Most HTTP libraries take care of this for you automatically but if - you are writing a low-level hook this is important to remember. -- GitLab ignores the HTTP status code returned by your endpoint. - -## Secret token - -If you specify a secret token, it will be sent with the hook request in the -`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify -that the request is legitimate. - -## SSL verification - -By default, the SSL certificate of the webhook endpoint is verified based on -an internal list of Certificate Authorities, which means the certificate cannot -be self-signed. - -You can turn this off in the webhook settings in your GitLab projects. - -![SSL Verification](ssl.png) - -## Events - -Below are described the supported events. - -### Push events - -Triggered when you push to the repository except when pushing tags. - -**Request header**: - -``` -X-Gitlab-Event: Push Hook -``` - -**Request body:** - -```json -{ - "object_kind": "push", - "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", - "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "ref": "refs/heads/master", - "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "user_id": 4, - "user_name": "John Smith", - "user_email": "john@example.com", - "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", - "project_id": 15, - "project":{ - "name":"Diaspora", - "description":"", - "web_url":"http://example.com/mike/diaspora", - "avatar_url":null, - "git_ssh_url":"git@example.com:mike/diaspora.git", - "git_http_url":"http://example.com/mike/diaspora.git", - "namespace":"Mike", - "visibility_level":0, - "path_with_namespace":"mike/diaspora", - "default_branch":"master", - "homepage":"http://example.com/mike/diaspora", - "url":"git@example.com:mike/diaspora.git", - "ssh_url":"git@example.com:mike/diaspora.git", - "http_url":"http://example.com/mike/diaspora.git" - }, - "repository":{ - "name": "Diaspora", - "url": "git@example.com:mike/diaspora.git", - "description": "", - "homepage": "http://example.com/mike/diaspora", - "git_http_url":"http://example.com/mike/diaspora.git", - "git_ssh_url":"git@example.com:mike/diaspora.git", - "visibility_level":0 - }, - "commits": [ - { - "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "message": "Update Catalan translation to e38cb41.", - "timestamp": "2011-12-12T14:27:31+02:00", - "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "author": { - "name": "Jordi Mallach", - "email": "jordi@softcatala.org" - }, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] - }, - { - "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "message": "fixed readme", - "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "author": { - "name": "GitLab dev user", - "email": "gitlabdev@dv6700.(none)" - }, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] - } - ], - "total_commits_count": 4 -} -``` - -### Tag events - -Triggered when you create (or delete) tags to the repository. - -**Request header**: - -``` -X-Gitlab-Event: Tag Push Hook -``` - -**Request body:** - -```json -{ - "object_kind": "tag_push", - "before": "0000000000000000000000000000000000000000", - "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", - "ref": "refs/tags/v1.0.0", - "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", - "user_id": 1, - "user_name": "John Smith", - "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", - "project_id": 1, - "project":{ - "name":"Example", - "description":"", - "web_url":"http://example.com/jsmith/example", - "avatar_url":null, - "git_ssh_url":"git@example.com:jsmith/example.git", - "git_http_url":"http://example.com/jsmith/example.git", - "namespace":"Jsmith", - "visibility_level":0, - "path_with_namespace":"jsmith/example", - "default_branch":"master", - "homepage":"http://example.com/jsmith/example", - "url":"git@example.com:jsmith/example.git", - "ssh_url":"git@example.com:jsmith/example.git", - "http_url":"http://example.com/jsmith/example.git" - }, - "repository":{ - "name": "Example", - "url": "ssh://git@example.com/jsmith/example.git", - "description": "", - "homepage": "http://example.com/jsmith/example", - "git_http_url":"http://example.com/jsmith/example.git", - "git_ssh_url":"git@example.com:jsmith/example.git", - "visibility_level":0 - }, - "commits": [], - "total_commits_count": 0 -} -``` - -### Issues events - -Triggered when a new issue is created or an existing issue was updated/closed/reopened. - -**Request header**: - -``` -X-Gitlab-Event: Issue Hook -``` - -**Request body:** - -```json -{ - "object_kind": "issue", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlabhq/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", - "namespace":"GitlabHQ", - "visibility_level":20, - "path_with_namespace":"gitlabhq/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlabhq/gitlab-test", - "url":"http://example.com/gitlabhq/gitlab-test.git", - "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "http_url":"http://example.com/gitlabhq/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://example.com/gitlabhq/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlabhq/gitlab-test" - }, - "object_attributes": { - "id": 301, - "title": "New API: create/update/delete file", - "assignee_id": 51, - "author_id": 51, - "project_id": 14, - "created_at": "2013-12-03T17:15:43Z", - "updated_at": "2013-12-03T17:15:43Z", - "position": 0, - "branch_name": null, - "description": "Create new API for manipulations with repository", - "milestone_id": null, - "state": "opened", - "iid": 23, - "url": "http://example.com/diaspora/issues/23", - "action": "open" - }, - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } -} -``` -### Comment events - -Triggered when a new comment is made on commits, merge requests, issues, and code snippets. -The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The -payload will also include information about the target of the comment. For example, -a comment on a issue will include the specific issue information under the `issue` key. -Valid target types: - -1. `commit` -2. `merge_request` -3. `issue` -4. `snippet` - -#### Comment on commit - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlabhq/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", - "namespace":"GitlabHQ", - "visibility_level":20, - "path_with_namespace":"gitlabhq/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlabhq/gitlab-test", - "url":"http://example.com/gitlabhq/gitlab-test.git", - "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "http_url":"http://example.com/gitlabhq/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1243, - "note": "This is a commit comment. How does this work?", - "noteable_type": "Commit", - "author_id": 1, - "created_at": "2015-05-17 18:08:09 UTC", - "updated_at": "2015-05-17 18:08:09 UTC", - "project_id": 5, - "attachment":null, - "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", - "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "noteable_id": null, - "system": false, - "st_diff": { - "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", - "new_path": "six", - "old_path": "six", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false - }, - "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" - }, - "commit": { - "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", - "timestamp": "2014-02-27T10:06:20+02:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "author": { - "name": "Dmitriy Zaporozhets", - "email": "dmitriy.zaporozhets@gmail.com" - } - } -} -``` - -#### Comment on merge request - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://localhost/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1244, - "note": "This MR needs work.", - "noteable_type": "MergeRequest", - "author_id": 1, - "created_at": "2015-05-17 18:21:36 UTC", - "updated_at": "2015-05-17 18:21:36 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 7, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" - }, - "merge_request": { - "id": 7, - "target_branch": "markdown", - "source_branch": "master", - "source_project_id": 5, - "author_id": 8, - "assignee_id": 28, - "title": "Tempora et eos debitis quae laborum et.", - "created_at": "2015-03-01 20:12:53 UTC", - "updated_at": "2015-03-21 18:27:27 UTC", - "milestone_id": 11, - "state": "opened", - "merge_status": "cannot_be_merged", - "target_project_id": 5, - "iid": 1, - "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", - "position": 0, - "locked_at": null, - "source":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "target": { - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "last_commit": { - "id": "562e173be03b8ff2efb05345d12df18815438a4b", - "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", - "timestamp": "2015-04-08T21: 00:25-07:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", - "author": { - "name": "John Smith", - "email": "john@example.com" - } - }, - "work_in_progress": false, - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } - } -} -``` - -#### Comment on issue - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name":"diaspora", - "url":"git@example.com:mike/diaspora.git", - "description":"", - "homepage":"http://example.com/mike/diaspora" - }, - "object_attributes": { - "id": 1241, - "note": "Hello world", - "noteable_type": "Issue", - "author_id": 1, - "created_at": "2015-05-17 17:06:40 UTC", - "updated_at": "2015-05-17 17:06:40 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 92, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" - }, - "issue": { - "id": 92, - "title": "test", - "assignee_id": null, - "author_id": 1, - "project_id": 5, - "created_at": "2015-04-12 14:53:17 UTC", - "updated_at": "2015-04-26 08:28:42 UTC", - "position": 0, - "branch_name": null, - "description": "test", - "milestone_id": null, - "state": "closed", - "iid": 17 - } -} -``` - -#### Comment on code snippet - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name":"Gitlab Test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "description":"Aut reprehenderit ut est.", - "homepage":"http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1245, - "note": "Is this snippet doing what it's supposed to be doing?", - "noteable_type": "Snippet", - "author_id": 1, - "created_at": "2015-05-17 18:35:50 UTC", - "updated_at": "2015-05-17 18:35:50 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 53, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" - }, - "snippet": { - "id": 53, - "title": "test", - "content": "puts 'Hello world'", - "author_id": 1, - "project_id": 5, - "created_at": "2015-04-09 02:40:38 UTC", - "updated_at": "2015-04-09 02:40:38 UTC", - "file_name": "test.rb", - "expires_at": null, - "type": "ProjectSnippet", - "visibility_level": 0 - } -} -``` - -### Merge request events - -Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. - -**Request header**: - -``` -X-Gitlab-Event: Merge Request Hook -``` - -**Request body:** - -```json -{ - "object_kind": "merge_request", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "object_attributes": { - "id": 99, - "target_branch": "master", - "source_branch": "ms-viewport", - "source_project_id": 14, - "author_id": 51, - "assignee_id": 6, - "title": "MS-Viewport", - "created_at": "2013-12-03T17:23:34Z", - "updated_at": "2013-12-03T17:23:34Z", - "st_commits": null, - "st_diffs": null, - "milestone_id": null, - "state": "opened", - "merge_status": "unchecked", - "target_project_id": 14, - "iid": 1, - "description": "", - "source":{ - "name":"Awesome Project", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/awesome_space/awesome_project", - "avatar_url":null, - "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", - "git_http_url":"http://example.com/awesome_space/awesome_project.git", - "namespace":"Awesome Space", - "visibility_level":20, - "path_with_namespace":"awesome_space/awesome_project", - "default_branch":"master", - "homepage":"http://example.com/awesome_space/awesome_project", - "url":"http://example.com/awesome_space/awesome_project.git", - "ssh_url":"git@example.com:awesome_space/awesome_project.git", - "http_url":"http://example.com/awesome_space/awesome_project.git" - }, - "target": { - "name":"Awesome Project", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/awesome_space/awesome_project", - "avatar_url":null, - "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", - "git_http_url":"http://example.com/awesome_space/awesome_project.git", - "namespace":"Awesome Space", - "visibility_level":20, - "path_with_namespace":"awesome_space/awesome_project", - "default_branch":"master", - "homepage":"http://example.com/awesome_space/awesome_project", - "url":"http://example.com/awesome_space/awesome_project.git", - "ssh_url":"git@example.com:awesome_space/awesome_project.git", - "http_url":"http://example.com/awesome_space/awesome_project.git" - }, - "last_commit": { - "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "message": "fixed readme", - "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "author": { - "name": "GitLab dev user", - "email": "gitlabdev@dv6700.(none)" - } - }, - "work_in_progress": false, - "url": "http://example.com/diaspora/merge_requests/1", - "action": "open", - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } - } -} -``` - -### Wiki Page events - -Triggered when a wiki page is created or edited. - -**Request Header**: - -``` -X-Gitlab-Event: Wiki Page Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "wiki_page", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" - }, - "project": { - "name": "awesome-project", - "description": "This is awesome", - "web_url": "http://example.com/root/awesome-project", - "avatar_url": null, - "git_ssh_url": "git@example.com:root/awesome-project.git", - "git_http_url": "http://example.com/root/awesome-project.git", - "namespace": "root", - "visibility_level": 0, - "path_with_namespace": "root/awesome-project", - "default_branch": "master", - "homepage": "http://example.com/root/awesome-project", - "url": "git@example.com:root/awesome-project.git", - "ssh_url": "git@example.com:root/awesome-project.git", - "http_url": "http://example.com/root/awesome-project.git" - }, - "wiki": { - "web_url": "http://example.com/root/awesome-project/wikis/home", - "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", - "git_http_url": "http://example.com/root/awesome-project.wiki.git", - "path_with_namespace": "root/awesome-project.wiki", - "default_branch": "master" - }, - "object_attributes": { - "title": "Awesome", - "content": "awesome content goes here", - "format": "markdown", - "message": "adding an awesome page to the wiki", - "slug": "awesome", - "url": "http://example.com/root/awesome-project/wikis/awesome", - "action": "create" - } -} -``` - -### Pipeline events - -Triggered on status change of Pipeline. - -**Request Header**: - -``` -X-Gitlab-Event: Pipeline Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "pipeline", - "object_attributes":{ - "id": 31, - "ref": "master", - "tag": false, - "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "status": "success", - "stages":[ - "build", - "test", - "deploy" - ], - "created_at": "2016-08-12 15:23:28 UTC", - "finished_at": "2016-08-12 15:26:29 UTC", - "duration": 63 - }, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "project":{ - "name": "Gitlab Test", - "description": "Atque in sunt eos similique dolores voluptatem.", - "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", - "avatar_url": null, - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", - "namespace": "Gitlab Org", - "visibility_level": 20, - "path_with_namespace": "gitlab-org/gitlab-test", - "default_branch": "master" - }, - "commit":{ - "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "message": "test\n", - "timestamp": "2016-08-12T17:23:21+02:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "author":{ - "name": "User", - "email": "user@gitlab.com" - } - }, - "builds":[ - { - "id": 380, - "stage": "deploy", - "name": "production", - "status": "skipped", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": null, - "finished_at": null, - "when": "manual", - "manual": true, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 377, - "stage": "test", - "name": "test-image", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:26:12 UTC", - "finished_at": null, - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 378, - "stage": "test", - "name": "test-build", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:26:12 UTC", - "finished_at": "2016-08-12 15:26:29 UTC", - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 376, - "stage": "build", - "name": "build-image", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:24:56 UTC", - "finished_at": "2016-08-12 15:25:26 UTC", - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 379, - "stage": "deploy", - "name": "staging", - "status": "created", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": null, - "finished_at": null, - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - } - ] -} -``` - -### Build events - -Triggered on status change of a Build. - -**Request Header**: - -``` -X-Gitlab-Event: Build Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "build", - "ref": "gitlab-script-trigger", - "tag": false, - "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "build_id": 1977, - "build_name": "test", - "build_stage": "test", - "build_status": "created", - "build_started_at": null, - "build_finished_at": null, - "build_duration": null, - "build_allow_failure": false, - "project_id": 380, - "project_name": "gitlab-org/gitlab-test", - "user": { - "id": 3, - "name": "User", - "email": "user@gitlab.com" - }, - "commit": { - "id": 2366, - "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "message": "test\n", - "author_name": "User", - "author_email": "user@gitlab.com", - "status": "created", - "duration": null, - "started_at": null, - "finished_at": null - }, - "repository": { - "name": "gitlab_test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "description": "Atque in sunt eos similique dolores voluptatem.", - "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", - "visibility_level": 20 - } -} -``` - -## Example webhook receiver - -If you want to see GitLab's webhooks in action for testing purposes you can use -a simple echo script running in a console session. For the following script to -work you need to have Ruby installed. - -Save the following file as `print_http_body.rb`: - -```ruby -require 'webrick' - -server = WEBrick::HTTPServer.new(:Port => ARGV.first) -server.mount_proc '/' do |req, res| - puts req.body -end - -trap 'INT' do - server.shutdown -end -server.start -``` - -Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb -8000`. Then add your server as a webhook receiver in GitLab as -`http://my.host:8000/`. - -When you press 'Test Hook' in GitLab, you should see something like this in the -console: - -``` -{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} -example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 -- -> / -``` +This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md). diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature deleted file mode 100644 index 41d79aa6ec8..00000000000 --- a/features/dashboard/shortcuts.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Shortcuts - Background: - Given I sign in as a user - And I visit dashboard page - - @javascript - Scenario: Navigate to projects tab - Given I press "g" and "p" - Then the active main tab should be Projects - - @javascript - Scenario: Navigate to issue tab - Given I press "g" and "i" - Then the active main tab should be Issues - - @javascript - Scenario: Navigate to merge requests tab - Given I press "g" and "m" - Then the active main tab should be Merge Requests - diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb deleted file mode 100644 index 118d27888df..00000000000 --- a/features/steps/dashboard/shortcuts.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedSidebarActiveTab - include SharedShortcuts -end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 374eb0b0e07..19ff92f6dc6 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps step 'recent build summary contains information saying that build has been erased' do page.within('.erased') do - expect(page).to have_content 'Build has been erased' + expect(page).to have_content 'Job has been erased' end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 7490d2bc6e7..48ac7a98f0d 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -34,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps step 'page should have CI graphs' do expect(page).to have_content 'Overall' - expect(page).to have_content 'Builds for last week' - expect(page).to have_content 'Builds for last month' - expect(page).to have_content 'Builds for last year' + expect(page).to have_content 'Jobs for last week' + expect(page).to have_content 'Jobs for last month' + expect(page).to have_content 'Jobs for last year' expect(page).to have_content 'Commit duration in minutes for last 30 commits' end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index d2fa8cd39af..9f0057cace7 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I fill in merge request search with "Fe"' do fill_in 'issuable_search', with: "Fe" + page.within '.merge-requests-holder' do + find('.merge-request') + end end step 'I click the "Target branch" dropdown' do diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 70e6d4836b2..d008a8a26af 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -47,7 +47,7 @@ module SharedBuilds end step 'recent build has a build trace' do - @build.trace = 'build trace' + @build.trace = 'job trace' end step 'download of build artifacts archive starts' do @@ -60,7 +60,7 @@ module SharedBuilds end step 'I see details of a build' do - expect(page).to have_content "Build ##{@build.id}" + expect(page).to have_content "Job ##{@build.id}" end step 'I see build trace' do diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 4ac491edc1b..13752eb4947 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -37,7 +37,7 @@ module API end desc 'Get the lists of a project board' do - detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13' + detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List end get '/lists' do diff --git a/lib/api/builds.rb b/lib/api/builds.rb index af61be343be..44fe0fc4a95 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -209,7 +209,7 @@ module API build = get_build!(params[:build_id]) - bad_request!("Unplayable Build") unless build.playable? + bad_request!("Unplayable Job") unless build.playable? build.play(current_user) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index e6d707f3c3d..2fefe760d24 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -54,7 +54,7 @@ module API authorize! :push_code, user_project attrs = declared_params - attrs[:source_branch] = attrs[:branch_name] + attrs[:start_branch] = attrs[:branch_name] attrs[:target_branch] = attrs[:branch_name] attrs[:actions].map! do |action| action[:action] = action[:action].to_sym @@ -139,8 +139,6 @@ module API commit_params = { commit: commit, create_merge_request: false, - source_project: user_project, - source_branch: commit.cherry_pick_branch_name, target_branch: params[:branch] } diff --git a/lib/api/files.rb b/lib/api/files.rb index 2e79e22e649..c58472de578 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -5,7 +5,7 @@ module API def commit_params(attrs) { file_path: attrs[:file_path], - source_branch: attrs[:branch_name], + start_branch: attrs[:branch_name], target_branch: attrs[:branch_name], commit_message: attrs[:commit_message], file_content: attrs[:content], diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a1d7b323f4f..eb5b947172a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -45,7 +45,7 @@ module API if id =~ /^\d+$/ Project.find_by(id: id) else - Project.find_with_namespace(id) + Project.find_by_full_path(id) end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index e8975eb57e0..080a6274957 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -30,7 +30,7 @@ module API def wiki? @wiki ||= project_path.end_with?('.wiki') && - !Project.find_with_namespace(project_path) + !Project.find_by_full_path(project_path) end def project @@ -41,7 +41,7 @@ module API # the wiki repository as well. project_path.chomp!('.wiki') if wiki? - Project.find_with_namespace(project_path) + Project.find_by_full_path(project_path) end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 9d8c5b63685..dcc0c82ee27 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -58,7 +58,7 @@ module API end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params + snippet_params = declared_params.merge(request: request, api: true) snippet_params[:content] = snippet_params.delete(:code) snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index e096e636806..eb9ece49e7f 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -64,7 +64,7 @@ module API desc: 'The visibility level of the snippet' end post do - attrs = declared_params(include_missing: false) + attrs = declared_params(include_missing: false).merge(request: request, api: true) snippet = CreateSnippetService.new(nil, current_user, attrs).execute if snippet.persisted? diff --git a/lib/api/users.rb b/lib/api/users.rb index 11a7368b4c0..0ed468626b7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -160,6 +160,8 @@ module API end end + user_params.merge!(password_expires_at: Time.now) if user_params[:password].present? + if user.update_attributes(user_params.except(:extern_uid, :provider)) present user, with: Entities::UserPublic else diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index 0257848b6bc..e2b57adf611 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -14,7 +14,7 @@ module Banzai def project_from_ref(ref) return context[:project] unless ref - Project.find_with_namespace(ref) + Project.find_by_full_path(ref) end end end diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb new file mode 100644 index 00000000000..e194cf59275 --- /dev/null +++ b/lib/banzai/filter/plantuml_filter.rb @@ -0,0 +1,39 @@ +require "nokogiri" +require "asciidoctor-plantuml/plantuml" + +module Banzai + module Filter + # HTML that replaces all `code plantuml` tags with PlantUML img tags. + # + class PlantumlFilter < HTML::Pipeline::Filter + def call + return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled + + plantuml_setup + + doc.css('pre.plantuml').each do |el| + img_tag = Nokogiri::HTML::DocumentFragment.parse( + Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {})) + el.replace img_tag + end + + doc + end + + private + + def settings + ApplicationSetting.current || ApplicationSetting.create_from_defaults + end + + def plantuml_setup + Asciidoctor::PlantUml.configure do |conf| + conf.url = settings.plantuml_url + conf.png_enable = settings.plantuml_enabled + conf.svg_enable = false + conf.txt_enable = false + end + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ac95a79009b..b25d6f18d59 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -10,6 +10,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SyntaxHighlightFilter, + Filter::PlantumlFilter, Filter::SanitizationFilter, Filter::MathFilter, diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7463bd719d5..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,6 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 730b05bed97..a10b4657d7d 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -8,6 +8,6 @@ class ProjectUrlConstrainer return false end - Project.find_with_namespace(full_path).present? + Project.find_by_full_path(full_path).present? end end diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb new file mode 100644 index 00000000000..12a063059cb --- /dev/null +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents Coverage settings. + # + class Coverage < Node + include Validatable + + validations do + validates :config, regexp: true + end + + def value + @config[1...-1] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index a4ec8f0ff2f..ede97cc0504 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,8 +33,11 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this pipeline.' + helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :jobs + :variables, :stages, :types, :cache, :coverage, :jobs def compose!(_deps = nil) super(self) do diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index a55362f0b6b..69a5e6f433d 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment] + after_script variables environment coverage] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,9 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment + :artifacts, :commands, :environment, :coverage attributes :script, :tags, :allow_failure, :when, :dependencies @@ -130,6 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, + coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index f01975aab5c..9b9a0a8125a 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -28,17 +28,21 @@ module Gitlab value.is_a?(String) || value.is_a?(Symbol) end + def validate_regexp(value) + !value.nil? && Regexp.new(value.to_s) && true + rescue RegexpError, TypeError + false + end + def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) if value.first == '/' && value.last == '/' - Regexp.new(value[1...-1]) + validate_regexp(value[1...-1]) else true end - rescue RegexpError - false end def validate_boolean(value) diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb index 28b0a9ffe01..16b234e6c59 100644 --- a/lib/gitlab/ci/config/entry/trigger.rb +++ b/lib/gitlab/ci/config/entry/trigger.rb @@ -9,15 +9,7 @@ module Gitlab include Validatable validations do - include LegacyValidationHelpers - - validate :array_of_strings_or_regexps - - def array_of_strings_or_regexps - unless validate_array_of_strings_or_regexps(config) - errors.add(:config, 'should be an array of strings or regexps') - end - end + validates :config, array_of_strings_or_regexps: true end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 8632dd0e233..bd7428b1272 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -54,6 +54,51 @@ module Gitlab end end + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + + private + + def look_like_regexp?(value) + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexpsValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return false unless value.is_a?(String) + return validate_regexp(value) if look_like_regexp?(value) + true + end + end + class TypeValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) type = options[:with] diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 127fae159d5..b8ec9138c10 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -34,7 +34,7 @@ module Gitlab end def project - @project ||= Project.find_with_namespace(project_path) + @project ||= Project.find_by_full_path(project_path) end private diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 3cd515e4a3a..d3df3f1bca1 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -6,7 +6,7 @@ module Gitlab class << self def ref_name(ref) - ref.gsub(/\Arefs\/(tags|heads)\//, '') + ref.sub(/\Arefs\/(tags|heads)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d32bdd86427..6babea144c7 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -30,11 +30,11 @@ module Gitlab def retrieve_project_and_type @type = :project - @project = Project.find_with_namespace(@repo_path) + @project = Project.find_by_full_path(@repo_path) if @repo_path.end_with?('.wiki') && !@project @type = :wiki - @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, '')) + @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, '')) end end diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb new file mode 100644 index 00000000000..3fe32adeade --- /dev/null +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -0,0 +1,24 @@ +# This Rack middleware is intended to proxy the webpack assets directory to the +# webpack-dev-server. It is only intended for use in development. + +module Gitlab + module Middleware + class WebpackProxy < Rack::Proxy + def initialize(app = nil, opts = {}) + @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) + end + + def perform_request(env) + unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + return @app.call(env) + end + + env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}" + super(env) + end + end + end +end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 786e1d49f5e..ef42b0557e0 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,5 +1,4 @@ require 'ruby-prof' -require_dependency 'gitlab/request_profiler' module Gitlab module RequestProfiler @@ -20,7 +19,7 @@ module Gitlab header_token = env['HTTP_X_PROFILE_TOKEN'] return unless header_token.present? - profile_token = RequestProfiler.profile_token + profile_token = Gitlab::RequestProfiler.profile_token return unless profile_token.present? header_token == profile_token diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb new file mode 100644 index 00000000000..7d5700b7f6d --- /dev/null +++ b/lib/rouge/lexers/plantuml.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Plantuml < Lexer + title "A passthrough lexer used for PlantUML input" + desc "A boring lexer that doesn't highlight anything" + + tag 'plantuml' + mimetypes 'text/plain' + + default_options token: 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 5d884bf9f66..b6ef8260191 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -3,6 +3,7 @@ namespace :gitlab do desc 'GitLab | Assets | Compile all frontend assets' task :compile do Rake::Task['assets:precompile'].invoke + Rake::Task['webpack:compile'].invoke Rake::Task['gitlab:assets:fix_urls'].invoke end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 4a696a52b4d..967f630ef20 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -58,7 +58,7 @@ namespace :gitlab do sub(%r{^/*}, ''). chomp('.git'). chomp('.wiki') - next if Project.find_with_namespace(repo_with_namespace) + next if Project.find_by_full_path(repo_with_namespace) new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect File.rename(path, new_path) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index a2eca74a3c8..b4015f5238e 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -29,7 +29,7 @@ namespace :gitlab do next end - project = Project.find_with_namespace(path) + project = Project.find_by_full_path(path) if project puts " * #{project.name} (#{repo_path}) exists" @@ -63,7 +63,7 @@ namespace :gitlab do if project.persisted? puts " * Created #{project.name} (#{repo_path})".color(:green) - ProjectCacheWorker.perform(project.id) + ProjectCacheWorker.perform_async(project.id) else puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) puts " Errors: #{project.errors.messages}".color(:red) diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index 7e2a6668e59..f2e12d85045 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -7,7 +7,7 @@ namespace :gitlab do unless args.project.present? abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]" end - project_path = Project.find_with_namespace(args.project).repository.path_to_repo + project_path = Project.find_by_full_path(args.project).repository.path_to_repo Sidekiq.redis do |redis| unless redis.exists(QUEUE) diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 4d4e746503a..84810b489ce 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -6,7 +6,7 @@ namespace :gitlab do %W(rake rubocop), %W(rake spinach), %W(rake spec), - %W(rake teaspoon) + %W(rake karma) ] cmds.each do |cmd| diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake new file mode 100644 index 00000000000..89812a179ec --- /dev/null +++ b/lib/tasks/karma.rake @@ -0,0 +1,25 @@ +unless Rails.env.production? + Rake::Task['karma'].clear if Rake::Task.task_defined?('karma') + + namespace :karma do + desc 'GitLab | Karma | Generate fixtures for JavaScript tests' + RSpec::Core::RakeTask.new(:fixtures) do |t| + ENV['NO_KNAPSACK'] = 'true' + t.pattern = 'spec/javascripts/fixtures/*.rb' + t.rspec_opts = '--format documentation' + end + + desc 'GitLab | Karma | Run JavaScript tests' + task :tests do + sh "npm run karma" do |ok, res| + abort('rake karma:tests failed') unless ok + end + end + end + + desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests' + task :karma do + Rake::Task['karma:fixtures'].invoke + Rake::Task['karma:tests'].invoke + end +end diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake deleted file mode 100644 index 08caedd7ff3..00000000000 --- a/lib/tasks/teaspoon.rake +++ /dev/null @@ -1,25 +0,0 @@ -unless Rails.env.production? - Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon') - - namespace :teaspoon do - desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests' - RSpec::Core::RakeTask.new(:fixtures) do |t| - ENV['NO_KNAPSACK'] = 'true' - t.pattern = 'spec/javascripts/fixtures/*.rb' - t.rspec_opts = '--format documentation' - end - - desc 'GitLab | Teaspoon | Run JavaScript tests' - task :tests do - require "teaspoon/console" - options = {} - abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures? - end - end - - desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests' - task :teaspoon do - Rake::Task['teaspoon:fixtures'].invoke - Rake::Task['teaspoon:tests'].invoke - end -end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index d3dcbd2c29b..3e01f91d32c 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -7,5 +7,5 @@ end unless Rails.env.production? desc "GitLab | Run all tests on CI with simplecov" - task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec] + task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec] end diff --git a/package.json b/package.json index 49b8210e427..73fb487b973 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,35 @@ { "private": true, "scripts": { + "dev-server": "node_modules/.bin/webpack-dev-server --config config/webpack.config.js", "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", - "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" + "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html", + "karma": "karma start config/karma.config.js --single-run", + "karma-start": "karma start config/karma.config.js" + }, + "dependencies": { + "babel": "^5.8.38", + "babel-core": "^5.8.38", + "babel-loader": "^5.4.2", + "bootstrap-sass": "3.3.6", + "compression-webpack-plugin": "^0.3.2", + "d3": "3.5.11", + "dropzone": "4.2.0", + "exports-loader": "^0.6.3", + "imports-loader": "^0.6.5", + "jquery": "2.2.1", + "jquery-ui": "github:jquery/jquery-ui#1.11.4", + "jquery-ujs": "1.2.1", + "json-loader": "^0.5.4", + "mousetrap": "1.4.6", + "select2": "3.5.2-browserify", + "stats-webpack-plugin": "^0.4.2", + "underscore": "1.8.3", + "vue": "2.0.3", + "vue-resource": "0.9.3", + "webpack": "^1.14.0", + "webpack-dev-server": "^1.16.2" }, "devDependencies": { "eslint": "^3.10.1", @@ -11,6 +37,13 @@ "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", - "istanbul": "^0.4.5" + "istanbul": "^0.4.5", + "jasmine-core": "^2.5.2", + "jasmine-jquery": "^2.1.1", + "karma": "^1.3.0", + "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^1.8.0" } } diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index 299d2c981d3..ad15e3942a5 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do end describe 'GET index' do - context 'with valid list id' do - it 'returns issues that have the list label applied' do - johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) - issue = create(:labeled_issue, project: project, labels: [planning]) - create(:labeled_issue, project: project, labels: [planning]) - create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) - create(:labeled_issue, project: project, labels: [development], assignee: johndoe) - issue.subscribe(johndoe, project) - - list_issues user: user, board: board, list: list2 - - parsed_response = JSON.parse(response.body) - - expect(response).to match_response_schema('issues') - expect(parsed_response.length).to eq 2 - end - end + let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } context 'with invalid board id' do it 'returns a not found 404 response' do @@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do end end - context 'with invalid list id' do - it 'returns a not found 404 response' do - list_issues user: user, board: board, list: 999 + context 'when list id is present' do + context 'with valid list id' do + it 'returns issues that have the list label applied' do + issue = create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) + create(:labeled_issue, project: project, labels: [development], assignee: johndoe) + issue.subscribe(johndoe, project) - expect(response).to have_http_status(404) + list_issues user: user, board: board, list: list2 + + parsed_response = JSON.parse(response.body) + + expect(response).to match_response_schema('issues') + expect(parsed_response.length).to eq 2 + end + end + + context 'with invalid list id' do + it 'returns a not found 404 response' do + list_issues user: user, board: board, list: 999 + + expect(response).to have_http_status(404) + end + end + end + + context 'when list id is missing' do + it 'returns opened issues without board labels applied' do + bug = create(:label, project: project, name: 'Bug') + create(:issue, project: project) + create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [development]) + create(:labeled_issue, project: project, labels: [bug]) + + list_issues user: user, board: board + + parsed_response = JSON.parse(response.body) + + expect(response).to match_response_schema('issues') + expect(parsed_response.length).to eq 2 end end @@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do end end - def list_issues(user:, board:, list:) + def list_issues(user:, board:, list: nil) sign_in(user) - get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param, - board_id: board.to_param, - list_id: list.to_param + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + board_id: board.to_param, + list_id: list.try(:to_param) + } + + get :index, params.compact end end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 34d6119429d..b3f9f76a50c 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do parsed_response = JSON.parse(response.body) expect(response).to match_response_schema('lists') - expect(parsed_response.length).to eq 3 + expect(parsed_response.length).to eq 2 end context 'with unauthorized user' do diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 32b0e42c3cd..19e948d8fb8 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -6,8 +6,8 @@ describe Projects::SnippetsController do let(:user2) { create(:user) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) end describe 'GET #index' do @@ -69,6 +69,86 @@ describe Projects::SnippetsController do end end + describe 'POST #create' do + def create_snippet(project, snippet_params = {}) + sign_in(user) + + project.add_developer(user) + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + project.add_master(admin) + sign_in(admin) + + post :mark_as_spam, + namespace_id: project.namespace.path, + project_id: project.path, + id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w[show raw].each do |action| describe "GET ##{action}" do context 'when the project snippet is private' do diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 99d0bcfa8d1..80f84a388ce 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,7 +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", "test 3", "master", false) + project.repository.commit_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master', update: false) end describe '#show' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 9323f723bdb..e7aa8745b99 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -213,6 +213,17 @@ describe ProjectsController do expect(response.status).to eq 404 end end + + context "redirection from http://someproject.git" do + it 'redirects to project page (format.html)' do + project = create(:project, :public) + + get :show, namespace_id: project.namespace.path, id: project.path, format: :git + + expect(response).to have_http_status(302) + expect(response).to redirect_to(namespace_project_path) + end + end end describe "#update" do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index d76fe9f580f..dadcb90cfc2 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -138,6 +138,65 @@ describe SnippetsController do end end + describe 'POST #create' do + def create_snippet(snippet_params = {}) + sign_in(user) + + post :create, { + personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + sign_in(admin) + + post :mark_as_spam, id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w(raw download).each do |action| describe "GET #{action}" do context 'when the personal snippet is private' do diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index ec46146d9b5..a581725245a 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -3,7 +3,6 @@ FactoryGirl.define do project factory: :empty_project after(:create) do |board| - board.lists.create(list_type: :backlog) board.lists.create(list_type: :done) end end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index bfe41f71b57..55727d6b62c 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -3,6 +3,18 @@ FactoryGirl.define do project factory: :empty_project author factory: :user + trait(:created) { action Event::CREATED } + trait(:updated) { action Event::UPDATED } + trait(:closed) { action Event::CLOSED } + trait(:reopened) { action Event::REOPENED } + trait(:pushed) { action Event::PUSHED } + trait(:commented) { action Event::COMMENTED } + trait(:merged) { action Event::MERGED } + trait(:joined) { action Event::JOINED } + trait(:left) { action Event::LEFT } + trait(:destroyed) { action Event::DESTROYED } + trait(:expired) { action Event::EXPIRED } + factory :closed_issue_event do action { Event::CLOSED } target factory: :closed_issue diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index 9e3f06c682c..2a2f3cca91c 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,12 +6,6 @@ FactoryGirl.define do sequence(:position) end - factory :backlog_list, parent: :list do - list_type :backlog - label nil - position nil - end - factory :done_list, parent: :list do list_type :done label nil diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 992580a6b34..715b2a27b30 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -106,6 +106,42 @@ FactoryGirl.define do path { 'gitlabhq' } test_repo + + transient do + create_template nil + end + + after :create do |project, evaluator| + TestEnv.copy_repo(project) + + if evaluator.create_template + args = evaluator.create_template + + project.add_user(args[:user], args[:access]) + + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/bug.md", + 'something valid', + message: 'test 3', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/template_test.md", + 'template_test', + message: 'test 1', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/feature_proposal.md", + 'feature_proposal', + message: 'test 2', + branch_name: 'master', + update: false) + end + end end factory :forked_project_with_submodules, parent: :empty_project do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index e177059d959..9d5ce876c29 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -9,8 +9,8 @@ describe 'Admin Builds' do let(:pipeline) { create(:ci_pipeline) } context 'All tab' do - context 'when have builds' do - it 'shows all builds' do + context 'when have jobs' do + it 'shows all jobs' do create(:ci_build, pipeline: pipeline, status: :pending) create(:ci_build, pipeline: pipeline, status: :running) create(:ci_build, pipeline: pipeline, status: :success) @@ -19,26 +19,26 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_selector('.row-content-block', text: 'All builds') + expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end end - context 'when have no builds' do + context 'when have no jobs' do it 'shows a message' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Pending tab' do - context 'when have pending builds' do - it 'shows pending builds' do + context 'when have pending jobs' do + it 'shows pending jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -55,22 +55,22 @@ describe 'Admin Builds' do end end - context 'when have no builds pending' do + context 'when have no jobs pending' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :pending) expect(page).to have_selector('.nav-links li.active', text: 'Pending') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Running tab' do - context 'when have running builds' do - it 'shows running builds' do + context 'when have running jobs' do + it 'shows running jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :running) build2 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :failed) @@ -87,22 +87,22 @@ describe 'Admin Builds' do end end - context 'when have no builds running' do + context 'when have no jobs running' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :running) expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Finished tab' do - context 'when have finished builds' do - it 'shows finished builds' do + context 'when have finished jobs' do + it 'shows finished jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -117,14 +117,14 @@ describe 'Admin Builds' do end end - context 'when have no builds finished' do + context 'when have no jobs finished' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :running) visit admin_builds_path(scope: :finished) expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel all' end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb new file mode 100644 index 00000000000..2875fc1e533 --- /dev/null +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -0,0 +1,233 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal', :feature, :js do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let!(:planning) { create(:label, project: project, name: 'Planning') } + let!(:label) { create(:label, project: project) } + let!(:list1) { create(:list, board: board, label: planning, position: 0) } + let!(:list2) { create(:list, board: board, label: label, position: 1) } + let!(:issue) { create(:issue, project: project) } + let!(:issue2) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + end + + context 'modal interaction' do + it 'opens modal' do + click_button('Add issues') + + expect(page).to have_selector('.add-issues-modal') + end + + it 'closes modal' do + click_button('Add issues') + + page.within('.add-issues-modal') do + find('.close').click + end + + expect(page).not_to have_selector('.add-issues-modal') + end + + it 'closes modal if cancel button clicked' do + click_button('Add issues') + + page.within('.add-issues-modal') do + click_button 'Cancel' + end + + expect(page).not_to have_selector('.add-issues-modal') + end + end + + context 'issues list' do + before do + click_button('Add issues') + + wait_for_vue_resource + end + + it 'loads issues' do + page.within('.add-issues-modal') do + page.within('.nav-links') do + expect(page).to have_content('2') + end + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'shows selected issues' do + page.within('.add-issues-modal') do + click_link 'Selected issues' + + expect(page).not_to have_selector('.card') + end + end + + context 'list dropdown' do + it 'resets after deleting list' do + page.within('.add-issues-modal') do + expect(find('.add-issues-footer')).to have_button(planning.title) + + click_button 'Cancel' + end + + first('.board-delete').click + + click_button('Add issues') + + wait_for_vue_resource + + page.within('.add-issues-modal') do + expect(find('.add-issues-footer')).not_to have_button(planning.title) + expect(find('.add-issues-footer')).to have_button(label.title) + end + end + end + + context 'search' do + it 'returns issues' do + page.within('.add-issues-modal') do + find('.form-control').native.send_keys(issue.title) + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'returns no issues' do + page.within('.add-issues-modal') do + find('.form-control').native.send_keys('testing search') + + expect(page).not_to have_selector('.card') + expect(page).not_to have_content("You haven't added any issues to your project yet") + end + end + end + + context 'selecing issues' do + it 'selects single issue' do + page.within('.add-issues-modal') do + first('.card').click + + page.within('.nav-links') do + expect(page).to have_content('Selected issues 1') + end + end + end + + it 'changes button text' do + page.within('.add-issues-modal') do + first('.card').click + + expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue') + end + end + + it 'changes button text with plural' do + page.within('.add-issues-modal') do + all('.card').each do |el| + el.click + end + + expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues') + end + end + + it 'shows only selected issues on selected tab' do + page.within('.add-issues-modal') do + first('.card').click + + click_link 'Selected issues' + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'selects all issues' do + page.within('.add-issues-modal') do + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + end + end + + it 'deselects all issues' do + page.within('.add-issues-modal') do + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + + click_button 'Deselect all' + + expect(page).not_to have_selector('.is-active') + end + end + + it 'selects all that arent already selected' do + page.within('.add-issues-modal') do + first('.card').click + + expect(page).to have_selector('.is-active', count: 1) + + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + end + end + + it 'unselects from selected tab' do + page.within('.add-issues-modal') do + first('.card').click + + click_link 'Selected issues' + + first('.card').click + + expect(page).not_to have_selector('.is-active') + end + end + end + + context 'adding issues' do + it 'adds to board' do + page.within('.add-issues-modal') do + first('.card').click + + click_button 'Add 1 issue' + end + + page.within(first('.board')) do + expect(page).to have_selector('.card') + end + end + + it 'adds to second list' do + page.within('.add-issues-modal') do + first('.card').click + + click_button planning.title + + click_link label.title + + click_button 'Add 1 issue' + end + + page.within(find('.board:nth-child(2)')) do + expect(page).to have_selector('.card') + end + end + end + end +end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index bfac5a1b8ab..34f47daf0e5 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do before do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'shows blank state' do @@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") end - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 1) end it 'creates default lists' do - lists = ['Backlog', 'To Do', 'Doing', 'Done'] + lists = ['To Do', 'Doing', 'Done'] page.within(find('.board-blank-state')) do click_button('Add default lists') end wait_for_vue_resource - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) page.all('.board').each_with_index do |list, i| expect(list.find('.board-title')).to have_content(lists[i]) @@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: development, position: 1) } - let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:issue1) { create(:issue, project: project, assignee: user) } - let!(:issue2) { create(:issue, project: project, author: user2) } - let!(:issue3) { create(:issue, project: project) } - let!(:issue4) { create(:issue, project: project) } + let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) } + let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) } + let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) } + let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) } let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) } let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) } + let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) } before do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) expect(find('.board:nth-child(1)')).to have_selector('.card') expect(find('.board:nth-child(2)')).to have_selector('.card') expect(find('.board:nth-child(3)')).to have_selector('.card') - expect(find('.board:nth-child(4)')).to have_selector('.card') end it 'shows lists' do - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) end it 'shows description tooltip on list title' do - page.within('.board:nth-child(2)') do + page.within('.board:nth-child(1)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end end it 'shows issues in lists' do + wait_for_board_cards(1, 8) wait_for_board_cards(2, 2) - wait_for_board_cards(3, 2) end it 'shows confidential issues with icon' do @@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'search backlog list' do - page.within('#js-boards-search') do - find('.form-control').set(issue1.title) - end - - wait_for_vue_resource - - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) - end - it 'search done list' do page.within('#js-boards-search') do find('.form-control').set(issue8.title) @@ -130,8 +116,7 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) end it 'search list' do @@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) end it 'allows user to delete board' do - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(1)')) do find('.board-delete').click end wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'removes checkmark in new list dropdown after deleting' do click_button 'Add list' wait_for_ajax - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(1)')) do find('.board-delete').click end wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) - expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active') + expect(page).to have_selector('.board', count: 2) end it 'infinite scrolls list' do 50.times do - create(:issue, project: project) + create(:labeled_issue, project: project, labels: [planning]) end visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('56') + expect(page.find('.board-header')).to have_content('58') expect(page).to have_selector('.card', count: 20) - expect(page).to have_content('Showing 20 of 56 issues') + expect(page).to have_content('Showing 20 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource expect(page).to have_selector('.card', count: 40) - expect(page).to have_content('Showing 40 of 56 issues') + expect(page).to have_content('Showing 40 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource - expect(page).to have_selector('.card', count: 56) + expect(page).to have_selector('.card', count: 58) expect(page).to have_content('Showing all issues') end end - context 'backlog' do - it 'shows issues in backlog with no labels' do - wait_for_board_cards(1, 6) - end - - it 'moves issue from backlog into list' do - drag_to(list_to_index: 1) - - wait_for_vue_resource - wait_for_board_cards(1, 5) - wait_for_board_cards(2, 3) - end - end - context 'done' do it 'shows list of done issues' do - wait_for_board_cards(4, 1) + wait_for_board_cards(3, 1) wait_for_ajax end it 'moves issue to done' do - drag_to(list_from_index: 0, list_to_index: 3) + drag_to(list_from_index: 0, list_to_index: 2) - wait_for_board_cards(1, 5) + wait_for_board_cards(1, 7) wait_for_board_cards(2, 2) wait_for_board_cards(3, 2) - wait_for_board_cards(4, 2) expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) - expect(find('.board:nth-child(4)')).to have_content(issue9.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) + expect(find('.board:nth-child(3)')).to have_content(issue9.title) + expect(find('.board:nth-child(3)')).not_to have_content(planning.title) end it 'removes all of the same issue to done' do - drag_to(list_from_index: 1, list_to_index: 3) + drag_to(list_from_index: 0, list_to_index: 2) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 1) - wait_for_board_cards(3, 1) - wait_for_board_cards(4, 2) + wait_for_board_cards(1, 7) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 2) - expect(find('.board:nth-child(2)')).not_to have_content(issue6.title) - expect(find('.board:nth-child(4)')).to have_content(issue6.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(3)')).to have_content(issue9.title) + expect(find('.board:nth-child(3)')).not_to have_content(planning.title) end end context 'lists' do it 'changes position of list' do - drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header') + drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header') - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 2) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 1) + wait_for_board_cards(1, 2) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 1) - expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(2)')).to have_content(planning.title) + expect(find('.board:nth-child(1)')).to have_content(development.title) + expect(find('.board:nth-child(1)')).to have_content(planning.title) end it 'issue moves between lists' do - drag_to(list_from_index: 1, card_index: 1, list_to_index: 2) + drag_to(list_from_index: 0, card_index: 1, list_to_index: 1) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 1) - wait_for_board_cards(3, 3) - wait_for_board_cards(4, 1) + wait_for_board_cards(1, 7) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 1) - expect(find('.board:nth-child(3)')).to have_content(issue6.title) - expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) + expect(find('.board:nth-child(2)')).to have_content(issue6.title) + expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title) end it 'issue moves between lists' do - drag_to(list_from_index: 2, list_to_index: 1) + drag_to(list_from_index: 1, list_to_index: 0) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 3) + wait_for_board_cards(1, 9) + wait_for_board_cards(2, 1) wait_for_board_cards(3, 1) - wait_for_board_cards(4, 1) - expect(find('.board:nth-child(2)')).to have_content(issue7.title) - expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) + expect(find('.board:nth-child(1)')).to have_content(issue7.title) + expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) end it 'issue moves from done' do - drag_to(list_from_index: 3, list_to_index: 1) + drag_to(list_from_index: 2, list_to_index: 1) expect(find('.board:nth-child(2)')).to have_content(issue8.title) - wait_for_board_cards(1, 6) + wait_for_board_cards(1, 8) wait_for_board_cards(2, 3) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 0) + wait_for_board_cards(3, 0) end context 'issue card' do @@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'creates new list for Backlog label' do @@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'creates new list for Done label' do @@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'keeps dropdown open after adding new list' do @@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.issue-boards-search')).to have_selector('.open') end - it 'moves issues from backlog into new list' do - wait_for_board_cards(1, 6) - - click_button 'Add list' - wait_for_ajax - - page.within('.dropdown-menu-issues-board-new') do - click_link testing.title - end - - wait_for_vue_resource - - wait_for_board_cards(1, 5) - end - it 'creates new list from a new label' do click_button 'Add list' @@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end end end @@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by assignee' do @@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by milestone' do @@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do end wait_for_vue_resource - wait_for_board_cards(1, 0) - wait_for_board_cards(2, 1) + wait_for_board_cards(1, 1) + wait_for_board_cards(2, 0) wait_for_board_cards(3, 0) - wait_for_board_cards(4, 0) end it 'filters by label' do @@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by label with space after reload' do @@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do it 'infinite scrolls list with label filter' do 50.times do - create(:labeled_issue, project: project, labels: [testing]) + create(:labeled_issue, project: project, labels: [planning, testing]) end page.within '.issues-filters' do @@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) - end - - it 'filters by no label' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link("No Label") - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end - - wait_for_vue_resource - - wait_for_board_cards(1, 5) - wait_for_board_cards(2, 0) - wait_for_board_cards(3, 0) - wait_for_board_cards(4, 1) + wait_for_empty_boards((2..3)) end it 'filters by clicking label button on issue' do page.within(find('.board', match: :first)) do - expect(page).to have_selector('.card', count: 6) + expect(page).to have_selector('.card', count: 8) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) wait_for_vue_resource @@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) page.within('.labels-filter') do expect(find('.dropdown-toggle-text')).to have_content(bug.title) diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index a03cd6fbf2d..6d14a8cf483 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } + let!(:list) { create(:list, board: board, position: 0) } let(:user) { create(:user) } context 'authorized user' do @@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'displays new issue button' do @@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do end it 'does not display new issue button in done list' do - page.within('.board:nth-child(3)') do + page.within('.board:nth-child(2)') do expect(page).not_to have_selector('.board-issue-count-holder .btn') end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c28bb0dcdae..9cc50167395 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do include WaitForAjax include WaitForVueResource - let(:project) { create(:empty_project, :public) } - let(:board) { create(:board, project: project) } - let(:user) { create(:user) } - let!(:label) { create(:label, project: project) } - let!(:label2) { create(:label, project: project) } - let!(:milestone) { create(:milestone, project: project) } - let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } - let!(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let!(:milestone) { create(:milestone, project: project) } + let!(:development) { create(:label, project: project, name: 'Development') } + let!(:bug) { create(:label, project: project, name: 'Bug') } + let!(:regression) { create(:label, project: project, name: 'Regression') } + let!(:stretch) { create(:label, project: project, name: 'Stretch') } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) } + let(:board) { create(:board, project: project) } + let!(:list) { create(:list, board: board, label: development, position: 0) } before do project.team << [user, :master] @@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do end page.within('.issue-boards-sidebar') do - expect(page).to have_content(issue.title) - expect(page).to have_content(issue.to_reference) + expect(page).to have_content(issue2.title) + expect(page).to have_content(issue2.to_reference) + end + end + + it 'removes card from board when clicking remove button' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.issue-boards-sidebar') do + click_button 'Remove from board' + end + + page.within(first('.board')) do + expect(page).to have_selector('.card', count: 1) end end @@ -244,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title + click_link bug.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 1) - expect(page).to have_content(label.title) + expect(page).to have_selector('.label', count: 3) + expect(page).to have_content(bug.title) end end page.within(first('.board')) do page.within(first('.card')) do - expect(page).to have_selector('.label', count: 1) - expect(page).to have_content(label.title) + expect(page).to have_selector('.label', count: 2) + expect(page).to have_content(bug.title) end end end @@ -274,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title - click_link label2.title + click_link bug.title + click_link regression.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(label.title) - expect(page).to have_content(label2.title) + expect(page).to have_selector('.label', count: 4) + expect(page).to have_content(bug.title) + expect(page).to have_content(regression.title) end end page.within(first('.board')) do page.within(first('.card')) do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(label.title) - expect(page).to have_content(label2.title) + expect(page).to have_selector('.label', count: 3) + expect(page).to have_content(bug.title) + expect(page).to have_content(regression.title) end end end it 'removes a label' do page.within(first('.board')) do - find('.card:nth-child(2)').click + first('.card').click end page.within('.labels') do @@ -307,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title + click_link stretch.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 0) - expect(page).not_to have_content(label.title) + expect(page).to have_selector('.label', count: 1) + expect(page).not_to have_content(stretch.title) end end page.within(first('.board')) do - page.within(find('.card:nth-child(2)')) do - expect(page).not_to have_selector('.label', count: 1) - expect(page).not_to have_content(label.title) + page.within(first('.card')) do + expect(page).not_to have_selector('.label') + expect(page).not_to have_content(stretch.title) end end end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb new file mode 100644 index 00000000000..d9be4e5dbdd --- /dev/null +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Dashboard shortcuts', feature: true, js: true do + before do + login_as :user + visit dashboard_projects_path + end + + scenario 'Navigate to tabs' do + find('body').native.send_key('g') + find('body').native.send_key('p') + + ensure_active_main_tab('Projects') + + find('body').native.send_key('g') + find('body').native.send_key('i') + + ensure_active_main_tab('Issues') + + find('body').native.send_key('g') + find('body').native.send_key('m') + + ensure_active_main_tab('Merge Requests') + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end +end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 31156fcf994..93139dc9e94 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user, username: 'someone.special') } + let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } @@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do expect(find('#at-view-64')).to have_selector('.cur:first-of-type') end + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys("@#{user.name[0...8]}") + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-64')).to have_content(user.name) + end + it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb new file mode 100644 index 00000000000..fc8515cfe9b --- /dev/null +++ b/spec/features/issues/group_label_sidebar_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe 'Group label on issue', :feature do + it 'renders link to the project issues page' do + group = create(:group) + project = create(:empty_project, :public, namespace: group) + feature = create(:group_label, group: group, title: 'feature') + issue = create(:labeled_issue, project: project, labels: [feature]) + label_link = namespace_project_issues_path( + project.namespace, + project, + label_name: [feature.name] + ) + + visit namespace_project_issue_path(project.namespace, project, issue) + + link = find('.issuable-show-labels a') + + expect(link[:href]).to eq(label_link) + end +end diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 7e2907cd26f..d2f5c4afc93 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end @@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 6958f6a2c9f..a2cf9b18bf2 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -10,19 +10,18 @@ feature 'toggler_behavior', js: true, feature: true do before do login_as :admin project = merge_request.source_project - visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" page.current_window.resize_to(1000, 300) + visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" end describe 'scroll position' do it 'should be scrolled down to fragment' do page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("document.querySelector('#{fragment_id}').getBoundingClientRect().top") - + fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true - expect(fragment_position_top).to be > page_scroll_y + expect(fragment_position_top).to be >= page_scroll_y expect(fragment_position_top).to be < (page_scroll_y + page_height) end end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 2582a540240..2f3c3e45ae6 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -120,5 +120,81 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do expect(page).not_to have_content '/due 2016-08-28' end end + + describe '/target_branch command in merge request' do + let(:another_project) { create(:project, :public) } + let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + + before do + logout + another_project.team << [user, :master] + login_with(user) + end + + it 'changes target_branch in new merge_request' do + visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) + click_button "Compare branches and continue" + + fill_in "merge_request_title", with: 'My brand new feature' + fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" + click_button "Submit merge request" + + merge_request = another_project.merge_requests.first + expect(merge_request.description).to eq "le feature \nFeature description:" + expect(merge_request.target_branch).to eq 'fix' + end + + it 'does not change target branch when merge request is edited' do + new_merge_request = create(:merge_request, source_project: another_project) + + visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request) + fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n" + click_button "Save changes" + + new_merge_request = another_project.merge_requests.first + expect(new_merge_request.description).to include('/target_branch') + expect(new_merge_request.target_branch).not_to eq('fix') + end + end + + describe '/target_branch command from note' do + context 'when the current user can change target branch' do + it 'changes target branch from a note' do + write_note("message start \n/target_branch merge-test\n message end.") + + expect(page).not_to have_content('/target_branch') + expect(page).to have_content('message start') + expect(page).to have_content('message end.') + + expect(merge_request.reload.target_branch).to eq 'merge-test' + end + + it 'does not fail when target branch does not exists' do + write_note('/target_branch totally_not_existing_branch') + + expect(page).not_to have_content('/target_branch') + + expect(merge_request.target_branch).to eq 'feature' + end + end + + context 'when current user can not change target branch' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not change target branch' do + write_note('/target_branch merge-test') + + expect(page).not_to have_content '/target_branch merge-test' + + expect(merge_request.target_branch).to eq 'feature' + end + end + end end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index b785b2f7704..fab2d532e06 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -89,7 +89,7 @@ describe 'Comments', feature: true do end end - it 'should reset the edit note form textarea with the original content of the note if cancelled' do + it 'resets the edit note form textarea with the original content of the note if cancelled' do within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' find('.btn-cancel').click @@ -198,7 +198,7 @@ describe 'Comments', feature: true do end describe 'the note form' do - it "shouldn't add a second form for same row" do + it "does not add a second form for same row" do click_diff_line is_expected. @@ -206,7 +206,7 @@ describe 'Comments', feature: true do count: 1) end - it 'should be removed when canceled' do + it 'is removed when canceled' do is_expected.to have_css('.js-temp-notes-holder') page.within("form[data-line-code='#{line_code}']") do diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 11d27feab0b..f7e0115643e 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -27,7 +27,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :pending) end - it "shows Pending tab builds" do + it "shows Pending tab jobs" do expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content build.short_sha @@ -42,7 +42,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :running) end - it "shows Running tab builds" do + it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_link 'Cancel running' expect(page).to have_content build.short_sha @@ -57,20 +57,20 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :finished) end - it "shows Finished tab builds" do + it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel running' end end - context "All builds" do + context "All jobs" do before do project.builds.running_or_pending.each(&:success) visit namespace_project_builds_path(project.namespace, project) end - it "shows All tab builds" do + it "shows All tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content build.short_sha expect(page).to have_content build.ref @@ -98,7 +98,7 @@ feature 'Builds', :feature do end describe "GET /:project/builds/:id" do - context "Build from project" do + context "Job from project" do before do visit namespace_project_build_path(project.namespace, project, build) end @@ -111,7 +111,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do visit namespace_project_build_path(project.namespace, project, build2) end @@ -149,7 +149,7 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - context 'when user has ability to update build' do + context 'when user has ability to update job' do it 'keeps artifacts when keep button is clicked' do expect(page).to have_content 'The artifacts will be removed' @@ -160,7 +160,7 @@ feature 'Builds', :feature do end end - context 'when user does not have ability to update build' do + context 'when user does not have ability to update job' do let(:user_access_level) { :guest } it 'does not have keep button' do @@ -197,8 +197,8 @@ feature 'Builds', :feature do visit namespace_project_build_path(project.namespace, project, build) end - context 'when build has an initial trace' do - it 'loads build trace' do + context 'when job has an initial trace' do + it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' build.append_trace(' and more trace', 11) @@ -242,32 +242,32 @@ feature 'Builds', :feature do end end - context 'when build starts environment' do + context 'when job starts environment' do let(:environment) { create(:environment, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) } - context 'build is successfull and has deployment' do + context 'job is successfull and has deployment' do let(:deployment) { create(:deployment) } let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build is complete and not successfull' do + context 'job is complete and not successfull' do let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build creates a new deployment' do + context 'job creates a new deployment' do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } @@ -281,7 +281,7 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/cancel" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -295,7 +295,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -307,13 +307,13 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/retry" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) click_link 'Cancel' page.within('.build-header') do - click_link 'Retry build' + click_link 'Retry job' end end diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index fe047e00409..36a80d7575d 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do let(:user) { create(:user) } let(:commit_params) do { - source_branch: project.default_branch, + start_branch: project.default_branch, target_branch: project.default_branch, commit_message: "Committing First Update", file_path: ".gitignore", 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 a521ce50f35..64094af29c0 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,8 @@ 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', 'Remove LICENSE', 'master') + project.repository.remove_file(project_master, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) visit namespace_project_path(project.namespace, project) diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 6dae5c64b30..e90a033b8c4 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,8 +18,20 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) - project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/test.md', + longtemplate_content, + message: 'added issue template', + branch_name: 'master', + update: false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -67,7 +79,13 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) 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 @@ -86,7 +104,13 @@ 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(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -111,7 +135,13 @@ 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(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + fork_user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) 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/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 917b545e98b..0b5ccc8c515 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -91,10 +91,10 @@ describe 'Pipeline', :feature, :js do end end - it 'should be possible to retry the success build' do + it 'should be possible to retry the success job' do find('#ci-badge-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end @@ -113,11 +113,11 @@ describe 'Pipeline', :feature, :js do it 'should be possible to retry the failed build' do find('#ci-badge-test .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end - context 'when pipeline has manual builds' do + context 'when pipeline has manual jobs' do it 'shows the skipped icon and a play action for the manual build' do page.within('#ci-badge-manual-build') do expect(page).to have_selector('.js-ci-status-icon-manual') @@ -129,14 +129,14 @@ describe 'Pipeline', :feature, :js do end end - it 'should be possible to play the manual build' do + it 'should be possible to play the manual job' do find('#ci-badge-manual-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Play build') + expect(page).not_to have_content('Play job') end end - context 'when pipeline has external build' do + context 'when pipeline has external job' do it 'shows the success icon and the generic comit status build' do expect(page).to have_selector('.js-ci-status-icon-success') expect(page).to have_content('jenkins') @@ -146,12 +146,12 @@ describe 'Pipeline', :feature, :js do end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end @@ -160,7 +160,7 @@ describe 'Pipeline', :feature, :js do end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -170,7 +170,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -191,7 +191,7 @@ describe 'Pipeline', :feature, :js do visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline) end - it 'shows a list of builds' do + it 'shows a list of jobs' do expect(page).to have_content('Test') expect(page).to have_content(build_passed.id) expect(page).to have_content('Deploy') @@ -203,26 +203,26 @@ describe 'Pipeline', :feature, :js do expect(page).to have_link('Play') end - it 'shows Builds tab pane as active' do + it 'shows jobs tab pane as active' do expect(page).to have_css('#js-tab-builds.active') end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end - it 'shows Builds tab as active' do + it 'shows Jobs tab as active' do expect(page).to have_css('li.js-builds-tab-link.active') end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -233,7 +233,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -244,7 +244,7 @@ describe 'Pipeline', :feature, :js do end end - context 'playing manual build' do + context 'playing manual job' do before do within '.pipeline-holder' do click_link('Play') diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 472491188c9..38fe2d92885 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -17,14 +17,15 @@ feature 'Ref switcher', feature: true, js: true do page.within '.project-refs-form' do input = find('input[type="search"]') - input.set 'expand' + input.set 'binary' + wait_for_ajax input.native.send_keys :down input.native.send_keys :down input.native.send_keys :enter end - expect(page).to have_title 'expand-collapse-files' + expect(page).to have_title 'binary-encoding' end it "user selects ref with special characters" do diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 4bfaa499272..034b75c2e51 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -11,41 +11,41 @@ feature 'Project settings > Merge Requests', feature: true, js: true do login_as(user) end - context 'when Merge Request and Builds are initially enabled' do + context 'when Merge Request and Pipelines are initially enabled' do before do project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) end - context 'when Builds are initially enabled' do + context 'when Pipelines are initially enabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings' do - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') end end - context 'when Builds are initially disabled' do + context 'when Pipelines are initially disabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings that do not depend on Builds feature' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end @@ -58,12 +58,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do end scenario 'does not show the Merge Requests settings' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index ad2d456529a..34f665826b6 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -10,15 +10,12 @@ describe ContributedProjectsFinder do let!(:private_project) { create(:empty_project, :private) } before do - private_project.team << [source_user, Gitlab::Access::MASTER] - private_project.team << [current_user, Gitlab::Access::DEVELOPER] - public_project.team << [source_user, Gitlab::Access::MASTER] + private_project.add_master(source_user) + private_project.add_developer(current_user) + public_project.add_master(source_user) - create(:event, action: Event::PUSHED, project: public_project, - target: public_project, author: source_user) - - create(:event, action: Event::PUSHED, project: private_project, - target: private_project, author: source_user) + create(:event, :pushed, project: public_project, target: public_project, author: source_user) + create(:event, :pushed, project: private_project, target: private_project, author: source_user) end describe 'without a current user' do diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 77f2bcee1f3..8e19cee5440 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -6,6 +6,7 @@ "confidential" ], "properties" : { + "id": { "type": "integer" }, "iid": { "type": "integer" }, "title": { "type": "string" }, "confidential": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 8d94cf26ecb..819287bf919 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["backlog", "label", "done"] + "enum": ["label", "done"] }, "label": { "type": ["object", "null"], diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 468bcc7badc..eae097126ce 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -134,7 +134,7 @@ describe DiffHelper do let(:new_pos) { 50 } let(:text) { 'some_text' } - it "should generate foldable top match line for inline view with empty text by default" do + it "generates foldable top match line for inline view with empty text by default" do output = diff_match_line old_pos, new_pos expect(output).to be_html_safe @@ -143,7 +143,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: '' end - it "should allow to define text and bottom option" do + it "allows to define text and bottom option" do output = diff_match_line old_pos, new_pos, text: text, bottom: true expect(output).to be_html_safe @@ -152,7 +152,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text end - it "should generate match line for parallel view" do + it "generates match line for parallel view" do output = diff_match_line old_pos, new_pos, text: text, view: :parallel expect(output).to be_html_safe @@ -162,7 +162,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text end - it "should allow to generate only left match line for parallel view" do + it "allows to generate only left match line for parallel view" do output = diff_match_line old_pos, nil, text: text, view: :parallel expect(output).to be_html_safe @@ -171,7 +171,7 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end - it "should allow to generate only right match line for parallel view" do + it "allows to generate only right match line for parallel view" do output = diff_match_line nil, new_pos, text: text, view: :parallel expect(output).to be_html_safe diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 3cd419b37c9..fbd9bb9f0ff 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -22,9 +22,10 @@ }, "plugins": ["jasmine"], "rules": { - "prefer-arrow-callback": 0, "func-names": 0, "jasmine/no-suite-dupes": [1, "branch"], - "jasmine/no-spec-dupes": [1, "branch"] + "jasmine/no-spec-dupes": [1, "branch"], + "no-console": 0, + "prefer-arrow-callback": 0 } } diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a2d57824585..76b370b345b 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,5 +1,5 @@ -/*= require lib/utils/text_utility */ -/*= require abuse_reports */ +require('~/lib/utils/text_utility'); +require('~/abuse_reports'); ((global) => { describe('Abuse Reports', () => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 7bc5b3268a0..e6a6fc36ca1 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,9 +1,8 @@ /* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ -/*= require js.cookie.js */ -/*= require jquery.endless-scroll.js */ -/*= require pager */ -/*= require activities */ +require('vendor/jquery.endless-scroll.js'); +require('~/pager'); +require('~/activities'); (() => { window.gon || (window.gon = {}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 71446b9df61..001cd8d6325 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,10 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ /* global AwardsHandler */ -/*= require awards_handler */ -/*= require jquery */ -/*= require js.cookie */ -/*= require ./fixtures/emoji_menu */ +require('~/awards_handler'); +require('./fixtures/emoji_menu'); (function() { var awardsHandler, lazyAssert, urlRoot; @@ -113,7 +111,7 @@ }); }); describe('::getAwardUrl', function() { - return it('should return the url for request', function() { + return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 51d911792ba..4a3da9e318b 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */ -/*= require behaviors/autosize */ +require('~/behaviors/autosize'); (function() { describe('Autosize behavior', function() { @@ -15,7 +15,7 @@ }); }); return load = function() { - return $(document).trigger('page:load'); + return $(document).trigger('load'); }; }); }).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0f046c2d83a..b84126c0e3d 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ -/*= require behaviors/quick_submit */ +require('~/behaviors/quick_submit'); (function() { describe('Quick Submit behavior', function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 9467056f04c..a958ac76e66 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var */ -/*= require behaviors/requires_input */ +require('~/behaviors/requires_input'); (function() { describe('requiresInput', function() { @@ -34,11 +34,5 @@ $('#required5').val('1').change(); return expect($('.submit')).not.toBeDisabled(); }); - return it('is called on page:load event', function() { - var spy; - spy = spyOn($.fn, 'requiresInput'); - $(document).trigger('page:load'); - return expect(spy).toHaveBeenCalled(); - }); }); }).call(this); diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 7c5850111cb..9dd741a680b 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,24 +6,19 @@ /* global listObj */ /* global listObjDuplicate */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data'); describe('Store', () => { beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); Cookies.set('issue_board_welcome_hidden', 'false', { @@ -61,18 +56,6 @@ describe('Store', () => { expect(list).toBeDefined(); }); - it('finds list limited by type', () => { - gl.issueBoards.BoardsStore.addList({ - id: 1, - position: 0, - title: 'Test', - list_type: 'backlog' - }); - const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog'); - - expect(list).toBeDefined(); - }); - it('gets issue when new list added', (done) => { gl.issueBoards.BoardsStore.addList(listObj); const list = gl.issueBoards.BoardsStore.findList('id', 1); @@ -117,10 +100,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); }); - it('check for blank state adding when backlog & done list exist', () => { - gl.issueBoards.BoardsStore.addList({ - list_type: 'backlog' - }); + it('check for blank state adding when done list exist', () => { gl.issueBoards.BoardsStore.addList({ list_type: 'done' }); diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6 new file mode 100644 index 00000000000..4340a571017 --- /dev/null +++ b/spec/javascripts/boards/issue_card_spec.js.es6 @@ -0,0 +1,191 @@ +/* global Vue */ +/* global ListUser */ +/* global ListLabel */ +/* global listObj */ +/* global ListIssue */ + +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/stores/boards_store'); +require('~/boards/components/issue_card_inner'); +require('./mock_data'); + +describe('Issue card component', () => { + const user = new ListUser({ + id: 1, + name: 'testing 123', + username: 'test', + avatar: 'test_image', + }); + const label1 = new ListLabel({ + id: 3, + title: 'testing 123', + color: 'blue', + text_color: 'white', + description: 'test', + }); + let component; + let issue; + let list; + + beforeEach(() => { + setFixtures('<div class="test-container"></div>'); + + list = listObj; + issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [list.label], + }); + + component = new Vue({ + el: document.querySelector('.test-container'), + data() { + return { + list, + issue, + issueLinkBase: '/test', + rootPath: '/', + }; + }, + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, + template: ` + <issue-card + :issue="issue" + :list="list" + :issue-link-base="issueLinkBase" + :root-path="rootPath"></issue-card> + `, + }); + }); + + it('renders issue title', () => { + expect( + component.$el.querySelector('.card-title').textContent, + ).toContain(issue.title); + }); + + it('includes issue base in link', () => { + expect( + component.$el.querySelector('.card-title a').getAttribute('href'), + ).toContain('/test'); + }); + + it('includes issue title on link', () => { + expect( + component.$el.querySelector('.card-title a').getAttribute('title'), + ).toBe(issue.title); + }); + + it('does not render confidential icon', () => { + expect( + component.$el.querySelector('.fa-eye-flash'), + ).toBeNull(); + }); + + it('renders confidential icon', (done) => { + component.issue.confidential = true; + + setTimeout(() => { + expect( + component.$el.querySelector('.confidential-icon'), + ).not.toBeNull(); + done(); + }, 0); + }); + + it('renders issue ID with #', () => { + expect( + component.$el.querySelector('.card-number').textContent, + ).toContain(`#${issue.id}`); + }); + + describe('assignee', () => { + it('does not render assignee', () => { + expect( + component.$el.querySelector('.card-assignee'), + ).toBeNull(); + }); + + describe('exists', () => { + beforeEach((done) => { + component.issue.assignee = user; + + setTimeout(() => { + done(); + }, 0); + }); + + it('renders assignee', () => { + expect( + component.$el.querySelector('.card-assignee'), + ).not.toBeNull(); + }); + + it('sets title', () => { + expect( + component.$el.querySelector('.card-assignee').getAttribute('title'), + ).toContain(`Assigned to ${user.name}`); + }); + + it('sets users path', () => { + expect( + component.$el.querySelector('.card-assignee').getAttribute('href'), + ).toBe('/test'); + }); + + it('renders avatar', () => { + expect( + component.$el.querySelector('.card-assignee img'), + ).not.toBeNull(); + }); + }); + }); + + describe('labels', () => { + it('does not render any', () => { + expect( + component.$el.querySelector('.label'), + ).toBeNull(); + }); + + describe('exists', () => { + beforeEach((done) => { + component.issue.addLabel(label1); + + setTimeout(() => { + done(); + }, 0); + }); + + it('does not render list label', () => { + expect( + component.$el.querySelectorAll('.label').length, + ).toBe(1); + }); + + it('renders label', () => { + expect( + component.$el.querySelector('.label').textContent, + ).toContain(label1.title); + }); + + it('sets label description as title', () => { + expect( + component.$el.querySelector('.label').getAttribute('title'), + ).toContain(label1.description); + }); + + it('sets background color of button', () => { + expect( + component.$el.querySelector('.label').style.backgroundColor, + ).toContain(label1.color); + }); + }); + }); +}); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index c8a61a0a9b5..aab4d9c501e 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,25 +2,20 @@ /* global BoardService */ /* global ListIssue */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data'); describe('Issue model', () => { let issue; beforeEach(() => { - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); issue = new ListIssue({ diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 7d942ec3d65..4397a32fedc 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,26 +5,21 @@ /* global List */ /* global listObj */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); +require('./mock_data'); describe('List model', () => { let list; beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); list = new List(listObj); diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 8d3e2237fda..7a399b307ad 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => { status: 200 })); }; + +window.listObj = listObj; +window.listObjDuplicate = listObjDuplicate; +window.BoardsMockData = BoardsMockData; +window.boardsMockInterceptor = boardsMockInterceptor; diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6 new file mode 100644 index 00000000000..1815847f3fa --- /dev/null +++ b/spec/javascripts/boards/modal_store_spec.js.es6 @@ -0,0 +1,132 @@ +/* global Vue */ +/* global ListIssue */ + +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/stores/modal_store'); + +describe('Modal store', () => { + let issue; + let issue2; + const Store = gl.issueBoards.ModalStore; + + beforeEach(() => { + // Setup default state + Store.store.issues = []; + Store.store.selectedIssues = []; + + issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [], + }); + issue2 = new ListIssue({ + title: 'Testing', + iid: 2, + confidential: false, + labels: [], + }); + Store.store.issues.push(issue); + Store.store.issues.push(issue2); + }); + + it('returns selected count', () => { + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles the issue as selected', () => { + Store.toggleIssue(issue); + + expect(issue.selected).toBe(true); + expect(Store.selectedCount()).toBe(1); + }); + + it('toggles the issue as un-selected', () => { + Store.toggleIssue(issue); + Store.toggleIssue(issue); + + expect(issue.selected).toBe(false); + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles all issues as selected', () => { + Store.toggleAll(); + + expect(issue.selected).toBe(true); + expect(issue2.selected).toBe(true); + expect(Store.selectedCount()).toBe(2); + }); + + it('toggles all issues as un-selected', () => { + Store.toggleAll(); + Store.toggleAll(); + + expect(issue.selected).toBe(false); + expect(issue2.selected).toBe(false); + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles all if a single issue is selected', () => { + Store.toggleIssue(issue); + Store.toggleAll(); + + expect(issue.selected).toBe(true); + expect(issue2.selected).toBe(true); + expect(Store.selectedCount()).toBe(2); + }); + + it('adds issue to selected array', () => { + issue.selected = true; + Store.addSelectedIssue(issue); + + expect(Store.selectedCount()).toBe(1); + }); + + it('removes issue from selected array', () => { + Store.addSelectedIssue(issue); + Store.removeSelectedIssue(issue); + + expect(Store.selectedCount()).toBe(0); + }); + + it('returns selected issue index if present', () => { + Store.toggleIssue(issue); + + expect(Store.selectedIssueIndex(issue)).toBe(0); + }); + + it('returns -1 if issue is not selected', () => { + expect(Store.selectedIssueIndex(issue)).toBe(-1); + }); + + it('finds the selected issue', () => { + Store.toggleIssue(issue); + + expect(Store.findSelectedIssue(issue)).toBe(issue); + }); + + it('does not find a selected issue', () => { + expect(Store.findSelectedIssue(issue)).toBe(undefined); + }); + + it('does not remove from selected issue if tab is not all', () => { + Store.store.activeTab = 'selected'; + + Store.toggleIssue(issue); + Store.toggleIssue(issue); + + expect(Store.store.selectedIssues.length).toBe(1); + expect(Store.selectedCount()).toBe(0); + }); + + it('gets selected issue array with only selected issues', () => { + Store.toggleIssue(issue); + Store.toggleIssue(issue2); + Store.toggleIssue(issue2); + + expect(Store.getSelectedIssues().length).toBe(1); + }); +}); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index ea953d0f5a5..fa9f95e16cd 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,6 +1,15 @@ -//= require lib/utils/bootstrap_linked_tabs +require('~/lib/utils/bootstrap_linked_tabs'); (() => { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + let phantomjs; + try { + phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + } catch (err) { + phantomjs = false; + } + describe('Linked Tabs', () => { preloadFixtures('static/linked_tabs.html.raw'); @@ -10,7 +19,9 @@ describe('when is initialized', () => { beforeEach(() => { - spyOn(window.history, 'replaceState').and.callFake(function () {}); + if (!phantomjs) { + spyOn(window.history, 'replaceState').and.callFake(function () {}); + } }); it('should activate the tab correspondent to the given action', () => { @@ -36,7 +47,7 @@ describe('on click', () => { it('should change the url according to the clicked tab', () => { - const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); + const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {}); const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line action: 'show', @@ -49,10 +60,11 @@ secondTab.click(); - expect(historySpy).toHaveBeenCalledWith({ - turbolinks: true, - url: newState, - }, document.title, newState); + if (historySpy) { + expect(historySpy).toHaveBeenCalledWith({ + url: newState, + }, document.title, newState); + } }); }); }); diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 0c556382980..0bd50588f5a 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -1,12 +1,11 @@ /* eslint-disable no-new */ /* global Build */ -/* global Turbolinks */ -//= require lib/utils/datetime_utility -//= require build -//= require breakpoints -//= require jquery.nicescroll -//= require turbolinks +require('~/lib/utils/datetime_utility'); +require('~/lib/utils/url_utility'); +require('~/build'); +require('~/breakpoints'); +require('vendor/jquery.nicescroll'); describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; @@ -167,7 +166,7 @@ describe('Build', () => { }); it('reloads the page when the build is done', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); jasmine.clock().tick(4001); const [{ success, context }] = $.ajax.calls.argsFor(1); @@ -177,7 +176,7 @@ describe('Build', () => { append: true, }); - expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); }); }); }); diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6 index bb9a9072f3a..05260760c43 100644 --- a/spec/javascripts/commits_spec.js.es6 +++ b/spec/javascripts/commits_spec.js.es6 @@ -1,10 +1,19 @@ /* global CommitsList */ -//= require jquery.endless-scroll -//= require pager -//= require commits +require('vendor/jquery.endless-scroll'); +require('~/pager'); +require('~/commits'); (() => { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + let phantomjs; + try { + phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + } catch (err) { + phantomjs = false; + } + describe('Commits List', () => { beforeEach(() => { setFixtures(` @@ -25,7 +34,10 @@ beforeEach(() => { CommitsList.init(25); CommitsList.searchField.val(''); - spyOn(history, 'replaceState').and.stub(); + + if (!phantomjs) { + spyOn(history, 'replaceState').and.stub(); + } ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { req.success({ data: '<li>Result</li>', diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 4d851b2d320..c0bdb89ed63 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,9 +1,7 @@ /* eslint-disable no-new */ -/*= require sidebar */ -/*= require jquery */ -/*= require js.cookie */ -/*= require lib/utils/text_utility */ +require('~/sidebar'); +require('~/lib/utils/text_utility'); ((global) => { describe('Dashboard', () => { diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 8ece24555c5..d5eec10be42 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/datetime_utility +require('~/lib/utils/datetime_utility'); (() => { describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index fbfa34a5da7..f956394ef53 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,10 +1,9 @@ /* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ -//= require vue -//= require diff_notes/models/discussion -//= require diff_notes/models/note -//= require diff_notes/stores/comments +require('~/diff_notes/models/discussion'); +require('~/diff_notes/models/note'); +require('~/diff_notes/stores/comments'); (() => { function createDiscussion(noteId = 1, resolved = true) { diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 056e4d41e93..b1838045a06 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,5 +1,4 @@ -//= require vue -//= require environments/components/environment_actions +require('~/environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 950a5d53fad..a6a587e69f5 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,5 +1,4 @@ -//= require vue -//= require environments/components/environment_external_url +require('~/environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index c178b9cc1ec..9858f346c83 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,6 +1,5 @@ -//= require vue -//= require timeago -//= require environments/components/environment_item +window.timeago = require('vendor/timeago'); +require('~/environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 95796f23894..043b8708a6e 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,5 +1,5 @@ -//= require vue -//= require environments/components/environment_rollback +require('~/environments/components/environment_rollback'); + describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 20e11ca3738..87eda136122 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -1,19 +1,17 @@ /* global Vue, environment */ -//= require vue -//= require vue-resource -//= require flash -//= require environments/stores/environments_store -//= require environments/components/environment -//= require ./mock_data +require('~/flash'); +require('~/environments/stores/environments_store'); +require('~/environments/components/environment'); +require('./mock_data'); describe('Environment', () => { - preloadFixtures('environments/environments'); + preloadFixtures('static/environments/environments.html.raw'); let component; beforeEach(() => { - loadFixtures('environments/environments'); + loadFixtures('static/environments/environments.html.raw'); }); describe('successfull request', () => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index bb998a32f32..2dfce5ba824 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,5 +1,5 @@ -//= require vue -//= require environments/components/environment_stop +require('~/environments/components/environment_stop'); + describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 17c00acf63e..9a8300d3832 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,8 +1,7 @@ /* global environmentsList */ -//= require vue -//= require environments/stores/environments_store -//= require ./mock_data +require('~/environments/stores/environments_store'); +require('./mock_data'); (() => { describe('Store', () => { diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 8ecd01f9a83..58f6fb96afb 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + const environmentsList = [ { id: 31, @@ -134,6 +134,8 @@ const environmentsList = [ }, ]; +window.environmentsList = environmentsList; + const environment = { id: 4, name: 'production', @@ -147,3 +149,5 @@ const environment = { created_at: '2016-12-16T11:51:04.690Z', updated_at: '2016-12-16T12:04:51.133Z', }; + +window.environment = environment; diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 3949c5615d5..ba5eb81defc 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var */ -/*= require extensions/array */ +require('~/extensions/array'); (function() { describe('Array extensions', function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index c5b86d35204..2d8a128ed33 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/element */ +require('~/extensions/element'); (() => { describe('Element extensions', function () { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 5cd0e5ab0f0..c0bb0419814 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var */ -/*= require extensions/jquery */ +require('~/extensions/jquery'); (function() { describe('jQuery extensions', function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 3b71c255b30..2467ed78459 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/object */ +require('~/extensions/object'); describe('Object extensions', () => { describe('assign', () => { diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 index 5eba4343a1d..10a316f31b4 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -1,7 +1,7 @@ -//= require filtered_search/dropdown_utils -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown -//= require filtered_search/dropdown_user +require('~/filtered_search/dropdown_utils'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown'); +require('~/filtered_search/dropdown_user'); (() => { describe('Dropdown User', () => { diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 89e49b7c511..1e2d7582d5b 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -1,7 +1,7 @@ -//= require extensions/array -//= require filtered_search/dropdown_utils -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/dropdown_utils'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); (() => { describe('Dropdown Utils', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index 4bd45eb457d..ed0b0196ec4 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); (() => { describe('Filtered Search Dropdown Manager', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index c8b5c2b36ad..98959dda242 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -1,11 +1,9 @@ -/* global Turbolinks */ - -//= require turbolinks -//= require lib/utils/common_utils -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager -//= require filtered_search/filtered_search_manager +require('~/lib/utils/url_utility'); +require('~/lib/utils/common_utils'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); +require('~/filtered_search/filtered_search_manager'); (() => { describe('Filtered Search Manager', () => { @@ -23,6 +21,7 @@ `); spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); @@ -37,7 +36,7 @@ it('should search with a single word', () => { getInput().value = 'searchTerm'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); }); @@ -47,7 +46,7 @@ it('should search with multiple words', () => { getInput().value = 'awesome search terms'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); }); @@ -57,7 +56,7 @@ it('should search with special characters', () => { getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); }); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 index 9d9097419ea..cf409a7e509 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -1,5 +1,5 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys'); (() => { describe('Filtered Search Token Keys', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index ac7f8e9cbcd..84c0e9cbfe2 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer'); (() => { describe('Filtered Search Tokenizer', () => { diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml index 1ea1725c561..59edc0396d2 100644 --- a/spec/javascripts/fixtures/environments/table.html.haml +++ b/spec/javascripts/fixtures/environments/table.html.haml @@ -3,7 +3,7 @@ %tr %th Environment %th Last deployment - %th Build + %th Job %th Commit %th %th diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index 99cebb32a8b..c61c32f8a13 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -1,6 +1,6 @@ -//= require gfm_auto_complete -//= require jquery -//= require jquery.atwho +require('~/gfm_auto_complete'); +require('vendor/jquery.caret'); +require('vendor/jquery.atwho'); const global = window.gl || (window.gl = {}); const GfmAutoComplete = global.GfmAutoComplete; diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 06fa64b1b4e..317f38c5888 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,11 +1,9 @@ /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ -/* global Turbolinks */ -/*= require jquery */ -/*= require gl_dropdown */ -/*= require turbolinks */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ +require('~/gl_dropdown'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); +require('~/lib/utils/url_utility'); (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; @@ -44,6 +42,7 @@ describe('Dropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ @@ -112,13 +111,13 @@ expect(this.dropdownContainerElement).toHaveClass('open'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { - spyOn(Turbolinks, 'visit').and.stub(); + spyOn(gl.utils, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); + if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); }); diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index f68fd9e00d7..733023481f5 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, arrow-body-style */ -//= require jquery -//= require gl_field_errors +require('~/gl_field_errors'); ((global) => { preloadFixtures('static/gl_field_errors.html.raw'); diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6 index b5f99483bfb..71d6e2a7e22 100644 --- a/spec/javascripts/gl_form_spec.js.es6 +++ b/spec/javascripts/gl_form_spec.js.es6 @@ -1,8 +1,9 @@ /* global autosize */ -/*= require gl_form */ -/*= require autosize */ -/*= require lib/utils/text_utility */ -/*= require lib/utils/common_utils */ + +window.autosize = require('vendor/autosize'); +require('~/gl_form'); +require('~/lib/utils/text_utility'); +require('~/lib/utils/common_utils'); describe('GLForm', () => { const global = window.gl || (window.gl = {}); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index d76fcc5206a..a954bb60560 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,7 +3,7 @@ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ -//= require graphs/stat_graph_contributors_graph +require('~/graphs/stat_graph_contributors_graph'); describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 63f28dfb8ad..b15764abe8c 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ /* global ContributorsStatGraphUtil */ -//= require graphs/stat_graph_contributors_util +require('~/graphs/stat_graph_contributors_util'); describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 71b589e6b83..876c23361bc 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes */ /* global StatGraph */ -//= require graphs/stat_graph +require('~/graphs/stat_graph'); describe("StatGraph", function () { describe("#get_log", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index b846c5ab00b..cecebb0b038 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var */ -/*= require header */ -/*= require lib/utils/text_utility */ -/*= require jquery */ + +require('~/header'); +require('~/lib/utils/text_utility'); (function() { describe('Header', function() { diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index 92a20687ec5..d3c37d39431 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - class ClassSpecHelper { static itShouldBeAStaticMethod(base, method) { return it('should be a static method', () => { @@ -7,3 +5,5 @@ class ClassSpecHelper { }); } } + +window.ClassSpecHelper = ClassSpecHelper; diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6 index d1155f1bd1e..0a61e561640 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper_spec.js.es6 @@ -1,5 +1,6 @@ /* global ClassSpecHelper */ -//= require ./class_spec_helper + +require('./class_spec_helper'); describe('ClassSpecHelper', () => { describe('.itShouldBeAStaticMethod', function () { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 917a6267b92..26d87cc5931 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,8 +1,7 @@ /* global Issuable */ -/* global Turbolinks */ -//= require issuable -//= require turbolinks +require('~/lib/utils/url_utility'); +require('~/issuable'); (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; @@ -42,39 +41,39 @@ }); it('should contain only the default parameters', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); Issuable.filterResults($filtersForm); - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); }); it('should filter for the phrase "broken"', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); updateForm({ search: 'broken' }, $filtersForm); Issuable.filterResults($filtersForm); const params = `${DEFAULT_PARAMS}&search=broken`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); it('should keep query parameters after modifying filter', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); // initial filter updateForm({ milestone_title: 'v1.0' }, $filtersForm); Issuable.filterResults($filtersForm); let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); // update filter updateForm({ label_name: 'Frontend' }, $filtersForm); Issuable.filterResults($filtersForm); params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); }); }); diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 index a1e979e8d09..cb068a4f879 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js.es6 +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -1,10 +1,11 @@ /* eslint-disable */ -//= require jquery -//= require vue -//= require issuable/time_tracking/components/time_tracker + +require('jquery'); +require('vue'); +require('~/issuable/time_tracking/components/time_tracker'); function initTimeTrackingComponent(opts) { - fixture.set(` + setFixtures(` <div> <div id="mock-container"></div> </div> diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 673a4b3c07a..5b0b7aa7903 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ /* global Issue */ -/*= require lib/utils/text_utility */ -/*= require issue */ +require('~/lib/utils/text_utility'); +require('~/issue'); (function() { var INVALID_URL = 'http://goesnowhere.nothing/whereami'; diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 0d19b4a25b9..37e038c16da 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -2,17 +2,15 @@ /* global IssuableContext */ /* global LabelsSelect */ -//= require lib/utils/type_utility -//= require jquery -//= require bootstrap -//= require gl_dropdown -//= require select2 -//= require jquery.nicescroll -//= require api -//= require create_label -//= require issuable_context -//= require users_select -//= require labels_select +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); +require('select2'); +require('vendor/jquery.nicescroll'); +require('~/api'); +require('~/create_label'); +require('~/issuable_context'); +require('~/users_select'); +require('~/labels_select'); (() => { let saveLabelCount = 0; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 1ce8f28e568..fbb06f3948b 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/common_utils +require('~/lib/utils/common_utils'); (() => { describe('common_utils', () => { @@ -10,9 +10,9 @@ // IE11 will return a relative pathname while other browsers will return a full pathname. // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute - // url starting with '/teaspoon'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); }); }); @@ -42,9 +42,13 @@ }); describe('gl.utils.getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); + it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('reporter'); - expect(value).toBe('Console'); + const value = gl.utils.getParameterByName('scope'); + expect(value).toBe('all'); }); it('should return invalid parameter', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index e97356b65d5..86ade66ec29 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/text_utility +require('~/lib/utils/text_utility'); (() => { describe('text_utility', () => { @@ -21,5 +21,19 @@ expect(largeFont > regular).toBe(true); }); }); + + describe('gl.text.pluralize', () => { + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 2)).toBe('tests'); + }); + + it('returns pluralized when count is 0', () => { + expect(gl.text.pluralize('test', 0)).toBe('tests'); + }); + + it('does not return pluralized', () => { + expect(gl.text.pluralize('test', 1)).toBe('test'); + }); + }); }); })(); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 6605986c33a..8b196f7720f 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */ /* global LineHighlighter */ -/*= require line_highlighter */ +require('~/line_highlighter'); (function() { describe('LineHighlighter', function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index f644d39b1c7..25cfa9e9479 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-return-assign */ /* global MergeRequest */ -/*= require merge_request */ +require('~/merge_request'); (function() { describe('MergeRequest', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 98201fb98ed..d20a59df041 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,11 +1,20 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ -/*= require merge_request_tabs */ -//= require breakpoints -//= require lib/utils/common_utils -//= require jquery.scrollTo +require('~/merge_request_tabs'); +require('~/breakpoints'); +require('~/lib/utils/common_utils'); +require('vendor/jquery.scrollTo'); (function () { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + var phantomjs; + try { + phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + } catch (err) { + phantomjs = false; + } + describe('MergeRequestTabs', function () { var stubLocation = {}; var setLocation = function (stubs) { @@ -22,9 +31,11 @@ this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); setLocation(); - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; + if (!phantomjs) { + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function () {}) + }; + } }); describe('#activateTab', function () { @@ -98,10 +109,11 @@ pathname: '/foo/bar/merge_requests/1' }); newState = this.subject('commits'); - expect(this.spies.history).toHaveBeenCalledWith({ - turbolinks: true, - url: newState - }, document.title, newState); + if (!phantomjs) { + expect(this.spies.history).toHaveBeenCalledWith({ + url: newState + }, document.title, newState); + } }); it('treats "show" like "notes"', function () { setLocation({ diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index bf45100af03..8cefdd2409d 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,7 +1,8 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ -/*= require merge_request_widget */ -/*= require lib/utils/datetime_utility */ +require('~/merge_request_widget'); +require('~/smart_interval'); +require('~/lib/utils/datetime_utility'); (function() { describe('MergeRequestWidget', function() { @@ -21,7 +22,11 @@ normal: "Build {{status}}" }, gitlab_icon: "gitlab_logo.png", - builds_path: "http://sampledomain.local/sampleBuildsPath" + ci_pipeline: 80, + ci_sha: "12a34bc5", + builds_path: "http://sampledomain.local/sampleBuildsPath", + commits_path: "http://sampledomain.local/commits", + pipeline_path: "http://sampledomain.local/pipelines" }; this["class"] = new window.gl.MergeRequestWidget(this.opts); }); @@ -118,10 +123,11 @@ }); }); - return describe('getCIStatus', function() { + describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { "title": "Sample MR title", + "pipeline": 80, "sha": "12a34bc5", "status": "success", "coverage": 98 @@ -165,6 +171,22 @@ this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); }); + it('should update the pipeline URL when the pipeline changes', function() { + var spy; + spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.pipeline += 1; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); + it('should update the commit URL when the sha changes', function() { + var spy; + spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.sha = "9b50b99a"; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); }); }); }).call(this); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index a1c2fe3df37..a6994f6edf4 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -//= require flash -//= require mini_pipeline_graph_dropdown +require('~/flash'); +require('~/mini_pipeline_graph_dropdown'); (() => { describe('Mini Pipeline Graph Dropdown', () => { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8259d553f1b..9b657868523 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ /* global NewBranchForm */ -/*= require jquery-ui/autocomplete */ -/*= require new_branch_form */ +require('jquery-ui/ui/autocomplete'); +require('~/new_branch_form'); (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 015c35dfca7..af495787c54 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,10 +1,10 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ /* global Notes */ -/*= require notes */ -/*= require autosize */ -/*= require gl_form */ -/*= require lib/utils/text_utility */ +require('~/notes'); +require('vendor/autosize'); +require('~/gl_form'); +require('~/lib/utils/text_utility'); (function() { window.gon || (window.gon = {}); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index f0f9ad7430d..72770a702d3 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,4 +1,9 @@ -//= require pipelines +require('~/pipelines'); + +// Fix for phantomJS +if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) { + Element.prototype.matches = Element.prototype.webkitMatchesSelector; +} (() => { describe('Pipelines', () => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 7a04fba5f7f..a4662cfb557 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/pretty_time +require('~/lib/utils/pretty_time'); (() => { const prettyTime = gl.utils.prettyTime; diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 0202c9ba85e..e0b52f767e4 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,14 +1,12 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ - /* global Project */ -/*= require bootstrap */ -/*= require select2 */ -/*= require lib/utils/type_utility */ -/*= require gl_dropdown */ -/*= require api */ -/*= require project_select */ -/*= require project */ +require('select2/select2.js'); +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); +require('~/api'); +require('~/project_select'); +require('~/project'); (function() { window.gon || (window.gon = {}); @@ -17,6 +15,8 @@ describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); + loadJSONFixtures('projects.json'); + beforeEach(function() { loadFixtures('static/project_title.html.raw'); return this.project = new Project(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 942778229b5..f7636865aa1 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,11 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ /* global Sidebar */ -/*= require right_sidebar */ -/*= require jquery */ -/*= require js.cookie */ - -/*= require extensions/jquery.js */ +require('~/right_sidebar'); +require('~/extensions/jquery.js'); (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; @@ -37,6 +34,8 @@ describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); + loadJSONFixtures('todos.json'); + beforeEach(function() { loadFixtures(fixtureName); this.sidebar = new Sidebar; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 7ac9710654f..c79e30e9481 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,13 +1,10 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ -/*= require gl_dropdown */ -/*= require search_autocomplete */ -/*= require jquery */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ -/*= require fuzzaldrin-plus */ -/*= require turbolinks */ -/*= require jquery.turbolinks */ +require('~/gl_dropdown'); +require('~/search_autocomplete'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); +require('vendor/fuzzaldrin-plus'); (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; @@ -117,13 +114,15 @@ preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() { loadFixtures('static/search_autocomplete.html.raw'); - return widget = new gl.SearchAutocomplete; + widget = new gl.SearchAutocomplete; + // Prevent turbolinks from triggering within gl_dropdown + spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); }); it('should show Dashboard specific dropdown menu', function() { var list; addBodyAttributes(); mockDashboardOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); }); @@ -131,7 +130,7 @@ var list; addBodyAttributes('group'); mockGroupOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, groupIssuesPath, groupMRsPath); }); @@ -139,7 +138,7 @@ var list; addBodyAttributes('project'); mockProjectOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, projectIssuesPath, projectMRsPath); }); @@ -148,7 +147,7 @@ addBodyAttributes('project'); mockProjectOptions(); widget.searchInput.val('help'); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; return expect(list.find(link).length).toBe(0); @@ -159,7 +158,7 @@ addBodyAttributes(); mockDashboardOptions(true); var submitSpy = spyOnEvent('form', 'submit'); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); widget.wrap.trigger($.Event('keydown', { which: DOWN })); var enterKeyEvent = $.Event('keydown', { which: ENTER }); widget.searchInput.trigger(enterKeyEvent); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index db11c2516a6..602ac01aec3 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */ /* global ShortcutsIssuable */ -/*= require copy_as_gfm */ -/*= require shortcuts_issuable */ +require('~/copy_as_gfm'); +require('~/shortcuts_issuable'); (function() { describe('ShortcutsIssuable', function() { @@ -59,12 +59,8 @@ expect(triggered).toBe(true); }); it('triggers `focus`', function() { - var focused = false; - $(this.selector).on('focus', function() { - focused = true; - }); this.shortcut.replyWithSelectedText(); - expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with a one-line selection', function() { diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index c274b9c45f4..d83d9a57b42 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require signin_tabs_memoizer */ +require('~/signin_tabs_memoizer'); ((global) => { describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 39d236986b9..4366ec2a5b8 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -1,5 +1,4 @@ -//= require jquery -//= require smart_interval +require('~/smart_interval'); (() => { const DEFAULT_MAX_INTERVAL = 100; @@ -164,7 +163,7 @@ const interval = this.smartInterval; setTimeout(() => { - $(document).trigger('page:before-unload'); + $(document).triggerHandler('beforeunload'); expect(interval.state.intervalId).toBeUndefined(); expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); done(); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js deleted file mode 100644 index f8e3aca29fa..00000000000 --- a/spec/javascripts/spec_helper.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable space-before-function-paren */ -// PhantomJS (Teaspoons default driver) doesn't have support for -// Function.prototype.bind, which has caused confusion. Use this polyfill to -// avoid the confusion. -/*= require support/bind-poly */ - -// You can require your own javascript files here. By default this will include -// everything in application, however you may get better load performance if you -// require the specific files that are being used in the spec that tests them. -/*= require jquery */ -/*= require jquery.turbolinks */ -/*= require bootstrap */ -/*= require underscore */ - -// Teaspoon includes some support files, but you can use anything from your own -// support path too. -// require support/jasmine-jquery-1.7.0 -// require support/jasmine-jquery-2.0.0 -/*= require support/jasmine-jquery-2.1.0 */ - -// require support/sinon -// require support/your-support-file -// Deferring execution -// If you're using CommonJS, RequireJS or some other asynchronous library you can -// defer execution. Call Teaspoon.execute() after everything has been loaded. -// Simple example of a timeout: -// Teaspoon.defer = true -// setTimeout(Teaspoon.execute, 1000) -// Matching files -// By default Teaspoon will look for files that match -// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path -// and it'll be included in the default suite automatically. If you want to -// customize suites, check out the configuration in teaspoon_env.rb -// Manifest -// If you'd rather require your spec files manually (to control order for -// instance) you can disable the suite matcher in the configuration and use this -// file as a manifest. -// For more information: http://github.com/modeset/teaspoon - -// set our fixtures path -jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures'; -jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures'; - -// defined in ActionDispatch::TestRequest -// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 -window.gl = window.gl || {}; -window.gl.TEST_HOST = 'http://test.host'; -window.gon = window.gon || {}; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index 99f45850ea3..454386697f5 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,9 +1,6 @@ /* eslint-disable max-len, arrow-parens, comma-dangle */ -//= vue -//= vue-resource -//= require jquery -//= require subbable_resource +require('~/subbable_resource'); /* * Test that each rest verb calls the publish and subscribe function and passes the correct value back diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 436f7064a69..c0c3837d1f4 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ -/*= require syntax_highlight */ +require('~/syntax_highlight'); (function() { describe('Syntax Highlighter', function() { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js new file mode 100644 index 00000000000..bf11ddbbea8 --- /dev/null +++ b/spec/javascripts/test_bundle.js @@ -0,0 +1,40 @@ +// enable test fixtures +require('jasmine-jquery'); + +jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; +jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; + +// include common libraries +window.$ = window.jQuery = require('jquery'); +window._ = require('underscore'); +window.Cookies = require('vendor/js.cookie'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('jquery-ujs'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); + +// stub expected globals +window.gl = window.gl || {}; +window.gl.TEST_HOST = 'http://test.host'; +window.gon = window.gon || {}; + +// render all of our tests +const testsContext = require.context('.', true, /_spec$/); +testsContext.keys().forEach(function (path) { + try { + testsContext(path); + } catch (err) { + console.error('[ERROR] WITH SPEC FILE: ', path); + console.error(err); + } +}); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 80163fd72d3..cba1af4daa4 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,11 +2,11 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -/*= require u2f/authenticate */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('~/u2f/authenticate'); +require('~/u2f/util'); +require('~/u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device'); (function() { describe('U2FAuthenticate', function() { @@ -25,19 +25,20 @@ document.querySelector('#js-login-2fa-device'), document.querySelector('.js-2fa-form') ); + + // bypass automatic form submission within renderAuthenticated + spyOn(this.component, 'renderAuthenticated').and.returnValue(true); + return this.component.start(); }); it('allows authenticating via a U2F device', function() { - var authenticatedMessage, deviceResponse, inProgressMessage; + var inProgressMessage; inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - deviceResponse = this.container.find('#js-device-response'); - expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.'); - return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); return describe("errors", function() { it("displays an error message", function() { @@ -51,7 +52,7 @@ return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); }); return it("allows retrying authentication after an error", function() { - var authenticatedMessage, retryButton, setupButton; + var retryButton, setupButton; setupButton = this.container.find("#js-login-u2f-device"); setupButton.trigger('click'); this.u2fDevice.respondToAuthenticateRequest({ @@ -64,8 +65,7 @@ this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated."); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); }); }); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0790553b67e..10578c2c4b5 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,11 +2,11 @@ /* global MockU2FDevice */ /* global U2FRegister */ -/*= require u2f/register */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('~/u2f/register'); +require('~/u2f/util'); +require('~/u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device'); (function() { describe('U2FRegister', function() { diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6 index b21f6912e06..9727c03c91e 100644 --- a/spec/javascripts/visibility_select_spec.js.es6 +++ b/spec/javascripts/visibility_select_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require visibility_select */ +require('~/visibility_select'); (() => { const VisibilitySelect = gl.VisibilitySelect; diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d6c6f786fb1..bbd914de4ea 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -//= require vue_common_component/commit +require('~/vue_common_component/commit'); describe('Commit component', () => { let props; diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 index 1a7f2bb5fb8..8935c474ee5 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -1,7 +1,5 @@ -//= require vue -//= require lib/utils/common_utils -//= require vue_pagination/index -/* global fixture, gl */ +require('~/lib/utils/common_utils'); +require('~/vue_pagination/index'); describe('Pagination component', () => { let component; @@ -17,7 +15,7 @@ describe('Pagination component', () => { }; it('should render and start at page 1', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -40,7 +38,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -61,7 +59,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -82,7 +80,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -103,7 +101,7 @@ describe('Pagination component', () => { }); it('should go to the first page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -124,7 +122,7 @@ describe('Pagination component', () => { }); it('should do nothing', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index be706ca304f..ce33a6814aa 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,7 @@ /* global Mousetrap */ /* global ZenMode */ -/*= require zen_mode */ +require('~/zen_mode'); (function() { var enterZen, escapeKeydown, exitZen; diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index 81b9a513ce3..deaabceef1c 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do it 'returns the referenced project' do project2 = double('referenced project') - expect(Project).to receive(:find_with_namespace). + expect(Project).to receive(:find_by_full_path). with('cross/reference').and_return(project2) expect(project_from_ref('cross/reference')).to eq project2 diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb new file mode 100644 index 00000000000..f85a5dcbd8b --- /dev/null +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Banzai::Filter::PlantumlFilter, lib: true do + include FilterSpecHelper + + it 'should replace plantuml pre tag with img tag' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if disabled' do + stub_application_setting(plantuml_enabled: false) + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if url is invalid' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index f824e2e1efe..49349035b3b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,33 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe '#build_attributes' do + context 'Coverage entry' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + let(:config_base) { { rspec: { script: "rspec" } } } + let(:config) { YAML.dump(config_base) } + + context 'when config has coverage set at the global scope' do + before do + config_base.update(coverage: '/\(\d+\.\d+\) covered/') + end + + context "and 'rspec' job doesn't have coverage set" do + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + end + + context "but 'rspec' job also has coverage set" do + before do + config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' + end + + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + end + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } @@ -21,6 +48,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: {}, allow_failure: false, @@ -435,6 +463,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -463,6 +492,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.5", @@ -702,6 +732,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -913,6 +944,7 @@ module Ci stage_idx: 1, name: "normal_job", commands: "test", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -958,6 +990,7 @@ module Ci stage_idx: 0, name: "job1", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -970,6 +1003,7 @@ module Ci stage_idx: 0, name: "job2", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index e3066311b7d..d70690f589d 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -5,15 +5,15 @@ describe EventFilter, lib: true do let(:source_user) { create(:user) } let!(:public_project) { create(:empty_project, :public) } - let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } - let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } - let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } - let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } - let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } - let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } - let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } - let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } - let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } + let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) } + let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) } + let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) } + let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) } + let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) } + let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) } + let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) } + let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) } + let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) } it 'applies push filter' do events = EventFilter.new(EventFilter.push).apply_filter(Event.all) diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb new file mode 100644 index 00000000000..4c6bd859552 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Coverage do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context "when entry config value doesn't have the surrounding '/'" do + let(:config) { 'Code coverage: \d+\.\d+' } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include(/coverage config must be a regular expression/) } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + + context "when entry config value has the surrounding '/'" do + let(:config) { '/Code coverage: \d+\.\d+/' } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq(config[1...-1]) } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context 'when entry value is not valid' do + let(:config) { '(malformed regexp' } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include(/coverage config must be a regular expression/) } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index e64c8d46bd8..d4f1780b174 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,12 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - it 'can contain global config keys' do - expect(described_class.nodes).to include :before_script + it 'returns a hash' do + expect(described_class.nodes).to be_a(Hash) end - it 'returns a hash' do - expect(described_class.nodes).to be_a Hash + context 'when filtering all the entry/node names' do + it 'contains the expected node names' do + node_names = described_class.nodes.keys + expect(node_names).to match_array(%i[before_script image services + after_script variables stages + types cache coverage]) + end end end @@ -35,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -176,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index fc9b8b86dc4..d20f4ec207d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -3,6 +3,20 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } + describe '.nodes' do + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script script stage type after_script cache + image services only except variables artifacts + environment coverage] + end + + it { is_expected.to match_array result } + end + end + describe 'validations' do before { entry.compose! } diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 1e21270d928..5893485634d 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with a diff file" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end @@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with diff lines" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index fe5fa048413..0f779339c54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do subject { described_class.new(diff_file) } describe '#parallelize' do - it 'should return an array of arrays containing the parsed diff' do + it 'returns an array of arrays containing the parsed diff' do diff_lines = diff_file.highlighted_diff_lines expected = [ # Unchanged lines diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index f5822fed37c..8e3e4034c8f 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::CreateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Create file", file_path: file_name, @@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::UpdateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Update file", file_path: file_name, @@ -125,7 +125,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::DeleteService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Delete file", file_path: file_name @@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do } merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request, options) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + project.commit(branch_name) end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index b080be62b34..116ab16ae74 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -209,7 +209,13 @@ 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(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false) + source_branch = project.repository.commit_file( + user, + FFaker::InternetSE.login_user_name, + FFaker::HipsterIpsum.paragraph, + message: FFaker::HipsterIpsum.sentence, + branch_name: unprotected_branch, + update: false) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index fadfe4d378e..e177d883158 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end - it 'should properly highlight all the lines' do + it 'highlights all the lines properly' do expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7fb6829f582..20241d4d63e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -52,6 +52,7 @@ snippets: - project - notes - award_emoji +- user_agent_detail releases: - project project_members: diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index d480c3821ec..1d65b24c2c9 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do project: project, commit_id: ci_pipeline.sha) - create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 493bc2db21a..95b230e4f5c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -222,6 +222,7 @@ CommitStatus: - queued_at - token - lock_version +- coverage_regex Ci::Variable: - id - project_id diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index b9d12c3c24c..9dd997aa7dc 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do expect(access).to receive(:block_user).with(user, 'does not exist anymore') access.allowed? diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 45cec65a284..1335a2b8f35 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -4,16 +4,14 @@ describe Gitlab::Template::IssueTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:file_path_1) { '.gitlab/issue_templates/bug.md' } - let(:file_path_2) { '.gitlab/issue_templates/template_test.md' } - let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + :repository, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'issue_templates' }) end describe '.all' do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index ae51b79be22..320b870309a 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -4,16 +4,14 @@ describe Gitlab::Template::MergeRequestTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' } - let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' } - let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + :repository, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'merge_request_templates' }) end describe '.all' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 47cd5075a7d..4080092405d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -221,6 +221,47 @@ describe Ci::Build, :models do end end + describe '#coverage_regex' do + subject { build.coverage_regex } + + context 'when project has build_coverage_regex set' do + let(:project_regex) { '\(\d+\.\d+\) covered' } + + before do + project.build_coverage_regex = project_regex + end + + context 'and coverage_regex attribute is not set' do + it { is_expected.to eq(project_regex) } + end + + context 'but coverage_regex attribute is also set' do + let(:build_regex) { 'Code coverage: \d+\.\d+' } + + before do + build.coverage_regex = build_regex + end + + it { is_expected.to eq(build_regex) } + end + end + + context 'when neither project nor build has coverage regex set' do + it { is_expected.to be_nil } + end + end + + describe '#update_coverage' do + context "regarding coverage_regex's value," do + it "saves the correct extracted coverage value" do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end + end + end + describe 'deployment' do describe '#last_deployment' do subject { build.last_deployment } @@ -443,11 +484,11 @@ describe Ci::Build, :models do let!(:build) { create(:ci_build, :trace, :success, :artifacts) } subject { build.erased? } - context 'build has not been erased' do + context 'job has not been erased' do it { is_expected.to be_falsey } end - context 'build has been erased' do + context 'job has been erased' do before do build.erase end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 3b7cc7d9e2e..9053485939e 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - deploy_master - end + merge_merge_requests_closing_issue(issue) + deploy_master expect(subject[:code].median).to be_nil end @@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:code].median).to be_nil end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5c73edbbc53..fc7d18bd40e 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do context "when a regular label (instead of a list label) is added to the issue" do it "returns nil" do - 5.times do - regular_label = create(:label) - issue = create(:issue, project: project) - issue.update(label_ids: [regular_label.id]) + regular_label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [regular_label.id]) - create_merge_request_closing_issue(issue) - merge_merge_requests_closing_issue(issue) - end + create_merge_request_closing_issue(issue) + merge_merge_requests_closing_issue(issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 591bbdddf55..b9fe492fe2c 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,7 +21,13 @@ 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(context.user, context.random_git_name, "content", "commit message", 'master', false) + sha = context.project.repository.commit_file( + context.user, + context.random_git_name, + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master @@ -29,11 +35,9 @@ describe 'CycleAnalytics#production', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:production].median).to be_nil end @@ -41,12 +45,10 @@ describe 'CycleAnalytics#production', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:production].median).to be_nil end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 33d2c0a7416..febb18c9884 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do context "when a regular merge request (that doesn't close the issue) is created and merged" do it "returns nil" do - 5.times do - MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) - end + MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) expect(subject[:review].median).to be_nil end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 00693d67475..9a024d533a1 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -29,10 +29,10 @@ describe 'CycleAnalytics#staging', feature: true do sha = context.project.repository.commit_file( context.user, context.random_git_name, - "content", - "commit message", - 'master', - false) + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master @@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:staging].median).to be_nil end @@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:staging].median).to be_nil end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index f857ea6cbec..c2ba012a0e6 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is for a regular merge request (that doesn't close an issue)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.succeed! + pipeline.run! + pipeline.succeed! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is not for a merge request" do it "returns nil" do - 5.times do - pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) + pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) - pipeline.run! - pipeline.succeed! - end + pipeline.run! + pipeline.succeed! expect(subject[:test].median).to be_nil end @@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is dropped (failed)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.drop! + pipeline.run! + pipeline.drop! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is cancelled" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.cancel! + pipeline.run! + pipeline.cancel! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 349474bb656..8c90a538f57 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -19,7 +19,7 @@ describe Event, models: true do let(:project) { create(:empty_project) } it 'calls the reset_project_activity method' do - expect_any_instance_of(Event).to receive(:reset_project_activity) + expect_any_instance_of(described_class).to receive(:reset_project_activity) create_event(project, project.owner) end @@ -43,33 +43,33 @@ describe Event, models: true do describe '#membership_changed?' do context "created" do - subject { build(:event, action: Event::CREATED).membership_changed? } + subject { build(:event, :created).membership_changed? } it { is_expected.to be_falsey } end context "updated" do - subject { build(:event, action: Event::UPDATED).membership_changed? } + subject { build(:event, :updated).membership_changed? } it { is_expected.to be_falsey } end context "expired" do - subject { build(:event, action: Event::EXPIRED).membership_changed? } + subject { build(:event, :expired).membership_changed? } it { is_expected.to be_truthy } end context "left" do - subject { build(:event, action: Event::LEFT).membership_changed? } + subject { build(:event, :left).membership_changed? } it { is_expected.to be_truthy } end context "joined" do - subject { build(:event, action: Event::JOINED).membership_changed? } + subject { build(:event, :joined).membership_changed? } it { is_expected.to be_truthy } end end describe '#note?' do - subject { Event.new(project: target.project, target: target) } + subject { described_class.new(project: target.project, target: target) } context 'issue note event' do let(:target) { create(:note_on_issue) } @@ -97,7 +97,7 @@ describe Event, models: true do let(:note_on_commit) { create(:note_on_commit, project: project) } let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } - let(:event) { Event.new(project: project, target: target, author_id: author.id) } + let(:event) { described_class.new(project: project, target: target, author_id: author.id) } before do project.team << [member, :developer] @@ -221,13 +221,13 @@ describe Event, models: true do let!(:event2) { create(:closed_issue_event) } describe 'without an explicit limit' do - subject { Event.limit_recent } + subject { described_class.limit_recent } it { is_expected.to eq([event2, event1]) } end describe 'with an explicit limit' do - subject { Event.limit_recent(1) } + subject { described_class.limit_recent(1) } it { is_expected.to eq([event2]) } end @@ -294,9 +294,9 @@ describe Event, models: true do } } - Event.create({ + described_class.create({ project: project, - action: Event::PUSHED, + action: described_class::PUSHED, data: data, author_id: user.id }.merge!(attrs)) diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 9e1a52011c3..e6ca4853873 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -19,13 +19,6 @@ describe List do expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id) end - context 'when list_type is set to backlog' do - subject { described_class.new(list_type: :backlog) } - - it { is_expected.not_to validate_presence_of(:label) } - it { is_expected.not_to validate_presence_of(:position) } - end - context 'when list_type is set to done' do subject { described_class.new(list_type: :done) } @@ -41,12 +34,6 @@ describe List do expect(subject.destroy).to be_truthy end - it 'can not be destroyed when list_type is set to backlog' do - subject = create(:backlog_list) - - expect(subject.destroy).to be_falsey - end - it 'can not be destroyed when when list_type is set to done' do subject = create(:done_list) @@ -55,19 +42,13 @@ describe List do end describe '#destroyable?' do - it 'retruns true when list_type is set to label' do + it 'returns true when list_type is set to label' do subject.list_type = :label expect(subject).to be_destroyable end - it 'retruns false when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject).not_to be_destroyable - end - - it 'retruns false when list_type is set to done' do + it 'returns false when list_type is set to done' do subject.list_type = :done expect(subject).not_to be_destroyable @@ -75,19 +56,13 @@ describe List do end describe '#movable?' do - it 'retruns true when list_type is set to label' do + it 'returns true when list_type is set to label' do subject.list_type = :label expect(subject).to be_movable end - it 'retruns false when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject).not_to be_movable - end - - it 'retruns false when list_type is set to done' do + it 'returns false when list_type is set to done' do subject.list_type = :done expect(subject).not_to be_movable @@ -102,12 +77,6 @@ describe List do expect(subject.title).to eq 'Development' end - it 'returns Backlog when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject.title).to eq 'Backlog' - end - it 'returns Done when list_type is set to done' do subject.list_type = :done diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 90d14c2c0b9..e4be0aba7a6 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -117,7 +117,7 @@ describe ProjectMember, models: true do users = create_list(:user, 2) described_class.add_users_to_projects( - [projects.first.id, projects.second], + [projects.first.id, projects.second.id], [users.first.id, users.second], described_class::MASTER) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 829b69093c9..53b98ba05f8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -15,7 +15,12 @@ describe Repository, models: true do let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) + repository.commit(merge_commit_id) end @@ -289,17 +294,39 @@ describe Repository, models: true do describe "#commit_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', 'Create newdir', 'master') + repository.commit_dir(user, 'newdir', + message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) newdir = repository.tree('master', 'newdir') expect(newdir.path).to eq('newdir') end + context "when committing to another project" do + let(:forked_project) { create(:project) } + + it "creates a fork and commit to the forked project" do + expect do + repository.commit_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) + + expect(repository.branch_exists?('patch')).to be_truthy + expect(forked_project.repository.branch_exists?('patch')).to be_falsy + + newdir = repository.tree('patch', 'newdir') + expect(newdir.path).to eq('newdir') + end + end + 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", "Add newdir", 'master', author_email: author_email, author_name: author_name) + repository.commit_dir(user, 'newdir', + message: 'Add newdir', + branch_name: 'master', + author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -314,8 +341,9 @@ describe Repository, models: true do it 'commits change to a file successfully' do expect do repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', - 'master', true) + message: 'Updates file content', + branch_name: 'master', + update: true) end.to change { repository.commits('master').count }.by(1) blob = repository.blob_at('master', 'CHANGELOG') @@ -326,8 +354,12 @@ 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!', 'Add README', - 'master', true, author_email: author_email, author_name: author_name) + repository.commit_file(user, 'README', '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) last_commit = repository.commit @@ -342,7 +374,7 @@ describe Repository, models: true do it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', - branch: 'master', + branch_name: 'master', previous_path: 'LICENSE', message: 'Changes filename') end.to change { repository.commits('master').count }.by(1) @@ -355,15 +387,16 @@ 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!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.update_file(user, 'README', "Updated README!", - branch: 'master', - previous_path: 'README', - message: 'Update README', - author_email: author_email, - author_name: author_name) + repository.update_file(user, 'README', 'Updated README!', + branch_name: 'master', + previous_path: 'README', + message: 'Update README', + author_email: author_email, + author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -376,10 +409,12 @@ describe Repository, models: true do describe "#remove_file" do it 'removes file successfully' do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master') + repository.remove_file(user, 'README', + message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) expect(repository.blob_at('master', 'README')).to be_nil @@ -387,10 +422,13 @@ 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!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name) + repository.remove_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) last_commit = repository.commit @@ -538,11 +576,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file( + user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file( + user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -551,21 +592,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'markdown') + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'markdown', update: false) 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!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) 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, Licensee::License.new('mit').content, "Add #{filename}", 'master', false) + repository.commit_file(user, filename, + Licensee::License.new('mit').content, + message: "Add #{filename}", branch_name: 'master', update: false) expect(repository.license_blob.name).to eq(filename) end @@ -574,7 +621,8 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') end it 'returns nil when no license is detected' do @@ -588,13 +636,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!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to eq('mit') end @@ -707,7 +758,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.rm_branch(user, 'new_feature') + repository.rm_branch(user, 'feature') end.to raise_error(GitHooksService::PreReceiveError) end @@ -728,36 +779,51 @@ describe Repository, models: true do context 'when pre hooks were successful' do before do - expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature'). - and_yield.and_return(true) + service = GitHooksService.new + expect(GitHooksService).to receive(:new).and_return(service) + expect(service).to receive(:execute). + with( + user, + repository.path_to_repo, + old_rev, + new_rev, + 'refs/heads/feature'). + and_yield(service).and_return(true) end it 'runs without errors' do expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - expect(repository).to receive(:update_autocrlf_option) + service = GitOperationService.new(user, repository) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + expect(service).to receive(:update_autocrlf_option) + + service.with_branch('feature') { new_rev } end context "when the branch wasn't empty" do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end + expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end end context 'when the update adds more than one commit' do - it 'runs without errors' do - old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } + it 'runs without errors' do # old_rev is an ancestor of new_rev expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) @@ -767,22 +833,28 @@ describe Repository, models: true do branch = 'feature-ff-target' repository.add_branch(user, branch, old_rev) - expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error + expect do + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end + end.not_to raise_error end end context 'when the update would remove commits from the target branch' do - it 'raises an exception' do - branch = 'master' - old_rev = repository.find_branch(branch).dereferenced_target.sha + let(:branch) { 'master' } + let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha } + it 'raises an exception' do # The 'master' branch is NOT an ancestor of new_rev. expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - repository.update_branch_with_hooks(user, branch) { new_rev } + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end end.to raise_error(Repository::CommitError) end end @@ -792,7 +864,9 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.to raise_error(GitHooksService::PreReceiveError) end end @@ -800,7 +874,6 @@ describe Repository, models: true do context 'when target branch is different from source branch' do before do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) - allow(repository).to receive(:update_ref!) end it 'expires branch cache' do @@ -809,7 +882,10 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - repository.update_branch_with_hooks(user, 'new-feature') { new_rev } + GitOperationService.new(user, repository). + with_branch('new-feature') do + new_rev + end end end @@ -827,7 +903,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_branches_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', 'master', false) + message: 'Updates file content', + branch_name: 'master', + update: false) end end end @@ -877,7 +955,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -892,7 +970,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=). with(:input) - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) end end end @@ -1009,8 +1087,11 @@ describe Repository, models: true do it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) - repository.commit(merge_commit_id) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end @@ -1388,9 +1469,10 @@ describe Repository, models: true do describe '#rm_tag' do it 'removes a tag' do expect(repository).to receive(:before_remove_tag) - expect(repository.rugged.tags).to receive(:delete).with('v1.1.0') - repository.rm_tag('v1.1.0') + repository.rm_tag(create(:user), 'v1.1.0') + + expect(repository.find_tag('v1.1.0')).to be_nil end end @@ -1458,16 +1540,16 @@ describe Repository, models: true do end end - describe '#update_ref!' do + describe '#update_ref' do it 'can create a ref' do - repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) end.to raise_error(Repository::CommitError) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6ca5ad747d1..6d58b1455c4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1013,8 +1013,8 @@ describe User, models: true do let!(:project2) { create(:empty_project, forked_from_project: project3) } let!(:project3) { create(:empty_project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } - let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) } - let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } + let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) } + let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) } before do project1.team << [subject, :master] @@ -1058,7 +1058,7 @@ describe User, models: true do let!(:push_data) do Gitlab::DataBuilder::Push.build_sample(project2, subject) end - let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) } + let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) } before do project1.team << [subject, :master] @@ -1086,7 +1086,7 @@ describe User, models: true do expect(subject.recent_push(project2)).to eq(push_event) push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) - push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1) + push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1) expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 645e36683bc..f197fadebab 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -67,7 +67,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'should not return project builds' do + it 'does not return project builds' do expect(response).to have_http_status(401) end end @@ -86,7 +86,7 @@ describe API::Builds, api: true do context 'when commit exists in repository' do context 'when user is authorized' do - context 'when pipeline has builds' do + context 'when pipeline has jobs' do before do create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) @@ -95,7 +95,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end - it 'returns project builds for specific commit' do + it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq 2 @@ -111,7 +111,7 @@ describe API::Builds, api: true do end end - context 'when pipeline has no builds' do + context 'when pipeline has no jobs' do before do branch_head = project.commit('feature').id get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) @@ -133,7 +133,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end - it 'does not return project builds' do + it 'does not return project jobs' do expect(response).to have_http_status(401) expect(json_response.except('message')).to be_empty end @@ -147,7 +147,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build data' do + it 'returns specific job data' do expect(response).to have_http_status(200) expect(json_response['name']).to eq('test') end @@ -165,7 +165,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build data' do + it 'does not return specific job data' do expect(response).to have_http_status(401) end end @@ -176,7 +176,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end - context 'build with artifacts' do + context 'job with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do @@ -185,7 +185,7 @@ describe API::Builds, api: true do 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end - it 'returns specific build artifacts' do + it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) end @@ -194,13 +194,13 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build artifacts' do + it 'does not return specific job artifacts' do expect(response).to have_http_status(401) end end end - it 'does not return build artifacts if not uploaded' do + it 'does not return job artifacts if not uploaded' do expect(response).to have_http_status(404) end end @@ -241,7 +241,7 @@ describe API::Builds, api: true do end end - context 'non-existing build' do + context 'non-existing job' do shared_examples 'not found' do it { expect(response).to have_http_status(:not_found) } end @@ -254,7 +254,7 @@ describe API::Builds, api: true do it_behaves_like 'not found' end - context 'has no such build' do + context 'has no such job' do before do get path_for_ref(pipeline.ref, 'NOBUILD') end @@ -263,7 +263,7 @@ describe API::Builds, api: true do end end - context 'find proper build' do + context 'find proper job' do shared_examples 'a valid file' do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', @@ -311,7 +311,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build trace' do + it 'returns specific job trace' do expect(response).to have_http_status(200) expect(response.body).to eq(build.trace) end @@ -320,7 +320,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build trace' do + it 'does not return specific job trace' do expect(response).to have_http_status(401) end end @@ -333,7 +333,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build persmission' do - it 'cancels running or pending build' do + it 'cancels running or pending job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') end @@ -342,7 +342,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(403) end end @@ -351,7 +351,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(401) end end @@ -366,7 +366,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build permission' do - it 'retries non-running build' do + it 'retries non-running job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') expect(json_response['status']).to eq('pending') @@ -376,7 +376,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(403) end end @@ -385,7 +385,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(401) end end @@ -396,23 +396,23 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/erase", user) end - context 'build is erasable' do + context 'job is erasable' do let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } - it 'erases build content' do + it 'erases job content' do expect(response.status).to eq 201 expect(build.trace).to be_empty expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy end - it 'updates build' do + it 'updates job' do expect(build.reload.erased_at).to be_truthy expect(build.reload.erased_by).to eq user end end - context 'build is not erasable' do + context 'job is not erasable' do let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'responds with forbidden' do @@ -452,20 +452,20 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/play", user) end - context 'on an playable build' do + context 'on an playable job' do let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - it 'plays the build' do + it 'plays the job' do expect(response).to have_http_status 200 expect(json_response['user']['id']).to eq(user.id) expect(json_response['id']).to eq(build.id) end end - context 'on a non-playable build' do + context 'on a non-playable job' do it 'returns a status code 400, Bad Request' do expect(response).to have_http_status 400 - expect(response.body).to match("Unplayable Build") + expect(response.body).to match("Unplayable Job") end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 1187d2e609d..a027c23bb88 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -326,7 +326,7 @@ describe API::Groups, api: true do expect(response).to have_http_status(404) end - it "should only return projects to which user has access" do + it "only returns projects to which user has access" do project3.team << [user3, :developer] get api("/groups/#{group1.id}/projects", user3) @@ -338,7 +338,7 @@ describe API::Groups, api: true do end context "when authenticated as admin" do - it "should return any existing group" do + it "returns any existing group" do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) @@ -346,7 +346,7 @@ describe API::Groups, api: true do expect(json_response.first['name']).to eq(project2.name) end - it "should not return a non existing group" do + it "does not return a non existing group" do get api("/groups/1328/projects", admin) expect(response).to have_http_status(404) @@ -354,7 +354,7 @@ describe API::Groups, api: true do end context 'when using group path in URL' do - it 'should return any existing group' do + it 'returns any existing group' do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 01032c0929b..45d5ae267c5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -4,6 +4,7 @@ describe API::ProjectSnippets, api: true do include ApiHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } let(:admin) { create(:admin) } describe 'GET /projects/:project_id/snippets/:id' do @@ -22,7 +23,7 @@ describe API::ProjectSnippets, api: true do let(:user) { create(:user) } it 'returns all snippets available to team member' do - project.team << [user, :developer] + project.add_developer(user) public_snippet = create(:project_snippet, :public, project: project) internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) @@ -50,7 +51,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -72,6 +73,51 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(project, snippet_params = {}) + project.add_developer(user) + + post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end end describe 'PUT /projects/:project_id/snippets/:id/' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1db81ce18c..753dde0ca3a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -459,7 +459,7 @@ describe API::Projects, api: true do before { project } before { admin } - it 'should create new project without path and return 201' do + it 'creates new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) expect(response).to have_http_status(201) end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f6fb6ea5506..6b9a739b439 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,7 +80,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -101,6 +101,36 @@ describe API::Snippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(snippet_params = {}) + post api('/snippets', user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'PUT /snippets/:id' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5bf5bf0739e..8692f9da976 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -305,6 +305,13 @@ describe API::Users, api: true do expect(user.reload.bio).to eq('new test bio') end + it "updates user with new password and forces reset on next login" do + put api("/users/#{user.id}", admin), password: '12345678' + + expect(response).to have_http_status(200) + expect(user.reload.password_expires_at).to be <= Time.now + end + it "updates user with organization" do put api("/users/#{user.id}", admin), { organization: 'GitLab' } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8dbe5f0b025..d85afdeab42 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -288,7 +288,7 @@ describe Ci::API::Builds do expect(build.reload.trace).to eq 'BUILD TRACE' end - context 'build has been erased' do + context 'job has been erased' do let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } it 'responds with forbidden' do @@ -458,7 +458,7 @@ describe Ci::API::Builds do before { build.run! } describe "POST /builds/:id/artifacts/authorize" do - context "should authorize posting artifact to running build" do + context "authorizes posting artifact to running build" do it "using token as parameter" do post authorize_url, { token: build.token }, headers @@ -492,7 +492,7 @@ describe Ci::API::Builds do end end - context "should fail to post too large artifact" do + context "fails to post too large artifact" do it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 77549db2927..96889abee79 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe 'project routing' do before do - allow(Project).to receive(:find_with_namespace).and_return(false) - allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true) + allow(Project).to receive(:find_by_full_path).and_return(false) + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true) end # Shared examples for a resource inside a Project @@ -86,13 +86,13 @@ describe 'project routing' do end context 'name with dot' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) } it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } end context 'with nested group' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) } it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } end diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index fde807cc410..7b29b043296 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do expect { service.execute }.to change(Board, :count).by(1) end - it 'creates default lists' do + it 'creates the default lists' do board = service.execute - expect(board.lists.size).to eq 2 - expect(board.lists.first).to be_backlog - expect(board.lists.last).to be_done + expect(board.lists.size).to eq 1 + expect(board.lists.first).to be_done end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 7c206cf3ce7..305278843f5 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } - let!(:backlog) { create(:backlog_list, board: board) } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:done) { create(:done_list, board: board) } @@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do end context 'sets default order to priority' do - it 'returns opened issues when listing issues from Backlog' do - params = { board_id: board.id, id: backlog.id } + it 'returns opened issues when list id is missing' do + params = { board_id: board.id } issues = described_class.new(project, user, params).execute diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index c43b2aec490..77f75167b3d 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do let(:development) { create(:label, project: project, name: 'Development') } let(:testing) { create(:label, project: project, name: 'Testing') } - let!(:backlog) { create(:backlog_list, board: board1) } let!(:list1) { create(:list, board: board1, label: development, position: 0) } let!(:list2) { create(:list, board: board1, label: testing, position: 1) } let!(:done) { create(:done_list, board: board1) } @@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do project.team << [user, :developer] end - context 'when moving from backlog' do - it 'adds the label of the list it goes to' do - issue = create(:labeled_issue, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id } - - described_class.new(project, user, params).execute(issue) - - expect(issue.reload.labels).to contain_exactly(bug, development) - end - end - - context 'when moving to backlog' do - it 'removes all list-labels' do - issue = create(:labeled_issue, project: project, labels: [bug, development, testing]) - params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id } - - described_class.new(project, user, params).execute(issue) - - expect(issue.reload.labels).to contain_exactly(bug) - end - end - - context 'when moving from backlog to done' do - it 'closes the issue' do - issue = create(:labeled_issue, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id } - - described_class.new(project, user, params).execute(issue) - issue.reload - - expect(issue.labels).to contain_exactly(bug) - expect(issue).to be_closed - end - end - context 'when moving an issue between lists' do let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } } @@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do end end - context 'when moving from done to backlog' do - it 'reopens the issue' do - issue = create(:labeled_issue, :closed, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id } - - described_class.new(project, user, params).execute(issue) - issue.reload - - expect(issue.labels).to contain_exactly(bug) - expect(issue).to be_reopened - end - end - context 'when moving to same list' do let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } } diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index a7e9efcf93f..ebac38e68f1 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do end end - context 'when board lists has backlog, and done lists' do + context 'when board lists has the done list' do it 'creates a new list at beginning of the list' do list = service.execute(board) @@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do end end - context 'when board lists has backlog, label and done lists' do + context 'when board lists has label and done lists' do it 'creates a new list at end of the label lists' do list1 = create(:list, board: board, position: 0) diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index 628caf03476..a30860f828a 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do end it 'decrements position of higher lists' do - backlog = board.backlog_list development = create(:list, board: board, position: 0) review = create(:list, board: board, position: 1) staging = create(:list, board: board, position: 2) @@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do described_class.new(project, user).execute(development) - expect(backlog.reload.position).to be_nil expect(review.reload.position).to eq 0 expect(staging.reload.position).to eq 1 expect(done.reload.position).to be_nil end end - it 'does not remove list from board when list type is backlog' do - list = board.backlog_list - service = described_class.new(project, user) - - expect { service.execute(list) }.not_to change(board.lists, :count) - end - it 'does not remove list from board when list type is done' do list = board.done_list service = described_class.new(project, user) diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index 334cee3f06d..2dffc62b215 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do service = described_class.new(project, double) - expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list] + expect(service.execute(board)).to eq [list, board.done_list] end end end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 63fa0bb8c5f..3786dc82bf0 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do let(:board) { create(:board, project: project) } let(:user) { create(:user) } - let!(:backlog) { create(:backlog_list, board: board) } let!(:planning) { create(:list, board: board, position: 0) } let!(:development) { create(:list, board: board, position: 1) } let!(:review) { create(:list, board: board, position: 2) } @@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do end end - it 'keeps position of lists when list type is backlog' do - service = described_class.new(project, user, position: 2) - - service.execute(backlog) - - expect(current_list_positions).to eq [0, 1, 2, 3] - end - it 'keeps position of lists when list type is done' do service = described_class.new(project, user, position: 2) diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb index 3760f19aaa2..0a7fc58523f 100644 --- a/spec/services/compare_service_spec.rb +++ b/spec/services/compare_service_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe CompareService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } - let(:service) { described_class.new } + let(:service) { described_class.new(project, 'feature') } describe '#execute' do context 'compare with base, like feature...fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: false) } + subject { service.execute(project, 'fix', straight: false) } it { expect(subject.diffs.size).to eq(1) } end context 'straight compare, like feature..fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: true) } + subject { service.execute(project, 'fix', straight: true) } it { expect(subject.diffs.size).to eq(3) } end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index b7dc99ed887..f2c2009bcbf 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -9,7 +9,7 @@ describe EventCreateService, services: true do it { expect(service.open_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } end end @@ -19,7 +19,7 @@ describe EventCreateService, services: true do it { expect(service.close_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } end end @@ -29,7 +29,7 @@ describe EventCreateService, services: true do it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } end end diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index d3c37c7820f..35e6e139238 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -6,7 +6,10 @@ describe Files::UpdateService do let(:project) { create(:project) } let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } - let(:new_contents) { "New Content" } + let(:new_contents) { 'New Content' } + let(:target_branch) { project.default_branch } + let(:last_commit_sha) { nil } + let(:commit_params) do { file_path: file_path, @@ -14,9 +17,9 @@ describe Files::UpdateService do file_content: new_contents, file_content_encoding: "text", last_commit_sha: last_commit_sha, - source_project: project, - source_branch: project.default_branch, - target_branch: project.default_branch, + start_project: project, + start_branch: project.default_branch, + target_branch: target_branch } end @@ -54,18 +57,6 @@ describe Files::UpdateService do end context "when the last_commit_sha is not supplied" do - let(:commit_params) do - { - file_path: file_path, - commit_message: "Update File", - file_content: new_contents, - file_content_encoding: "text", - source_project: project, - source_branch: project.default_branch, - target_branch: project.default_branch, - } - end - it "returns a hash with the :success status " do results = subject.execute @@ -80,5 +71,15 @@ describe Files::UpdateService do expect(results.data).to eq(new_contents) end end + + context 'when target branch is different than source branch' do + let(:target_branch) { "#{project.default_branch}-new" } + + it 'fires hooks only once' do + expect(GitHooksService).to receive(:new).once.and_call_original + + subject.execute + end + end end end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 41b0968b8b4..3318dfb22b6 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -21,7 +21,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil]) + service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 5f6a7716beb..d55a7657c0e 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request).to be_closed } - it 'should execute hooks with close action' do + it 'executes hooks with close action' do expect(service).to have_received(:execute_hooks). with(@merge_request, 'close') end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 314ea670a71..2cc21acab7b 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -89,7 +89,7 @@ describe MergeRequests::RefreshService, services: true do # Merge master -> feature branch author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } commit_options = { message: 'Test message', committer: author, author: author } - @project.repository.merge(@user, @merge_request, commit_options) + @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options) commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index 388abb6a0df..a0e51681725 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -66,7 +66,13 @@ 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(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + project.repository.commit_file( + user, + 'new-file-in-target', + '', + message: 'Add new file in target', + branch_name: 'conflict-start', + update: false) end before do diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 66fc8fc360b..0b0925983eb 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end end + + context '/target_branch command' do + let(:non_empty_project) { create(:project) } + let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } + let(:service) { described_class.new(non_empty_project, developer)} + + it 'updates target_branch if /target_branch command is executed' do + _, updates = service.execute('/target_branch merge-test', merge_request) + + expect(updates).to eq(target_branch: 'merge-test') + end + + it 'handles blanks around param' do + _, updates = service.execute('/target_branch merge-test ', merge_request) + + expect(updates).to eq(target_branch: 'merge-test') + end + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/target_branch' } + let(:issuable) { another_merge_request } + end + end + + context 'ignores non-existing target branch' do + it_behaves_like 'empty command' do + let(:content) { '/target_branch totally_non_existing_branch' } + let(:issuable) { another_merge_request } + end + end + end end end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 75c95d70951..6ed55289ed9 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -35,7 +35,13 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false) + sha = project.repository.commit_file( + user, + random_git_name, + 'content', + message: 'commit message', + branch_name: source_branch, + update: false) project.repository.commit(sha) opts = { diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 10b90b40ba7..19b32c84d81 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -63,22 +63,20 @@ module CycleAnalyticsHelpers # test case. allow(self).to receive(:project) { other_project } - 5.times do - data = data_fn[self] - start_time = Time.now - end_time = rand(1..10).days.from_now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now + end_time = rand(1..10).days.from_now - end_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(end_time) { condition_fn[self, data] } - end + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original @@ -114,17 +112,15 @@ module CycleAnalyticsHelpers context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - end_time = rand(1..10).days.from_now - - end_time_conditions.each_with_index do |(condition_name, condition_fn), index| - Timecop.freeze(end_time + index.days) { condition_fn[self, data] } - end + data = data_fn[self] + end_time = rand(1..10).days.from_now - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each_with_index do |(condition_name, condition_fn), index| + Timecop.freeze(end_time + index.days) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + expect(subject[phase].median).to be_nil end end @@ -133,17 +129,15 @@ module CycleAnalyticsHelpers context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - start_time = Time.now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now - post_fn[self, data] if post_fn + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } end + post_fn[self, data] if post_fn + expect(subject[phase].median).to be_nil end end diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index 1b0a4583f5c..944ea30656f 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -35,7 +35,7 @@ module ExportFileHelper project: project, commit_id: ci_pipeline.sha) - create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + create(:event, :created, target: milestone, project: project, author: user) create(:project_member, :master, user: user, project: project) create(:ci_variable, project: project) create(:ci_trigger, project: project) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 90f1a9c8798..b87232a350b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -36,7 +36,8 @@ module TestEnv 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', 'deleted-image-test' => '6c17798', - 'wip' => 'b9238ee' + 'wip' => 'b9238ee', + 'csv' => '3dd0896' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 80fc8c48fed..8d1cff7a261 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" end - it 'should run the task without errors' do + it 'runs the task without errors' do expect { run_rake_task }.not_to raise_error end end diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb deleted file mode 100644 index 643b161cdf4..00000000000 --- a/spec/teaspoon_env.rb +++ /dev/null @@ -1,178 +0,0 @@ -Teaspoon.configure do |config| - # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to - # `http://localhost:3000/jasmine` to run your tests. - config.mount_at = "/teaspoon" - - # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can - # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). - # Note: Defaults to `Rails.root` if nil. - config.root = nil - - # Paths that will be appended to the Rails assets paths - # Note: Relative to `config.root`. - config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] - - # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will - # be rendered as fixtures. - config.fixture_paths = ["spec/javascripts/fixtures"] - - # SUITES - # - # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. - # - # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can - # omit various directives and the ones defined in the default suite will be used. - # - # To run a specific suite - # - in the browser: http://localhost/teaspoon/[suite_name] - # - with the rake task: rake teaspoon suite=[suite_name] - # - with the cli: teaspoon --suite=[suite_name] - config.suite do |suite| - # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for - # you -- which you can override with the directives below. This should be specified first, as it can override other - # directives. - # Note: If no version is specified, the latest is assumed. - # - # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 - suite.use_framework :jasmine, "2.2.0" - - # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These - # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" - - # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. - # suite.javascripts = [] - - # You can include your own stylesheets if you want to change how Teaspoon looks. - # Note: Spec related CSS can and should be loaded using fixtures. - # suite.stylesheets = ["teaspoon"] - - # This suites spec helper, which can require additional support files. This file is loaded before any of your test - # files are loaded. - suite.helper = "spec_helper" - - # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating - # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. - # - # Available: boot, boot_require_js - suite.boot_partial = "boot" - - # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. - suite.body_partial = "body" - - # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a - # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. - # suite.hook :fixtures, &proc{} - - # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated - # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, - # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. - # suite.expand_assets = true - end - - # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also - # be run in the default suite -- but can be focused into a more specific suite. - # config.suite :targeted do |suite| - # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" - # end - - # CONSOLE RUNNER SPECIFIC - # - # These configuration directives are applicable only when running via the rake task or command line interface. These - # directives can be overridden using the command line interface arguments or with ENV variables when using the rake - # task. - # - # Command Line Interface: - # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js - # - # Rake: - # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite - - # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. - # - # Available: :phantomjs, :selenium, :capybara_webkit - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - # config.driver = :phantomjs - - # Specify additional options for the driver. - # - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - # config.driver_options = nil - - # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be - # considered a failure. This is to avoid issues that can arise where tests stall. - # config.driver_timeout = 180 - - # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. - # config.server = nil - - # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. - # config.server_port = nil - - # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may - # want to lower this if you know it shouldn't take long to start. - # config.server_timeout = 20 - - # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have - # several suites, but in environments like CI this may not be desirable. - # config.fail_fast = true - - # Specify the formatters to use when outputting the results. - # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. - # - # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity - # config.formatters = [:dot] - - # Specify if you want color output from the formatters. - # config.color = true - - # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to - # remove them, but in verbose applications this may not be desirable. - # config.suppress_log = false - - # COVERAGE REPORTS / THRESHOLD ASSERTIONS - # - # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and - # display coverage statistics. - # - # Coverage configurations are similar to suites. You can define several, and use different ones under different - # conditions. - # - # To run with a specific coverage configuration - # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] - # - with the cli: teaspoon --coverage=[coverage_name] - - # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage - # on the CLI. - # Set this to "true" or the name of your coverage config. - config.use_coverage = true - - # You can have multiple coverage configs by passing a name to config.coverage. - # e.g. config.coverage :ci do |coverage| - # The default coverage config name is :default. - config.coverage do |coverage| - # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. - # - # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - coverage.reports = ["text-summary", "html"] - - # The path that the coverage should be written to - when there's an artifact to write to disk. - # Note: Relative to `config.root`. - coverage.output_path = "coverage-javascript" - - # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The - # default excludes assets from vendor, gems and support libraries. - coverage.ignore = [%r{vendor/}, %r{spec/javascripts/(?!helpers/)}] - - # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any - # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. - # coverage.statements = nil - # coverage.functions = nil - # coverage.branches = nil - # coverage.lines = nil - 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 44870cfcfb3..b6f6e7b7a2b 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -15,7 +15,7 @@ describe 'projects/builds/show', :view do allow(view).to receive(:can?).and_return(true) end - describe 'build information in header' do + describe 'job information in header' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -28,11 +28,11 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.ci-status.ci-success', text: 'passed') end - it 'does not render a link to the build' do + it 'does not render a link to the job' do expect(rendered).not_to have_link('passed') end - it 'shows build id' do + it 'shows job id' do expect(rendered).to have_css('.js-build-id', text: build.id) end @@ -45,8 +45,8 @@ describe 'projects/builds/show', :view do end end - describe 'environment info in build view' do - context 'build with latest deployment' do + describe 'environment info in job view' do + context 'job with latest deployment' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -57,7 +57,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is the most recent deployment' + expected_text = 'This job is the most recent deployment' render expect(rendered).to have_css( @@ -65,7 +65,7 @@ describe 'projects/builds/show', :view do end end - context 'build with outdated deployment' do + context 'job with outdated deployment' do let(:build) do create(:ci_build, :success, environment: 'staging', pipeline: pipeline) end @@ -87,7 +87,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is an out-of-date deployment ' \ + expected_text = 'This job is an out-of-date deployment ' \ "to staging.\nView the most recent deployment ##{second_deployment.iid}." render @@ -95,7 +95,7 @@ describe 'projects/builds/show', :view do end end - context 'build failed to deploy' do + context 'job failed to deploy' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -105,7 +105,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed.' + expected_text = 'The deployment of this job to staging did not succeed.' render expect(rendered).to have_css( @@ -113,7 +113,7 @@ describe 'projects/builds/show', :view do end end - context 'build will deploy' do + context 'job will deploy' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -124,7 +124,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -137,7 +137,7 @@ describe 'projects/builds/show', :view do end it 'shows that deployment will be overwritten' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -150,7 +150,7 @@ describe 'projects/builds/show', :view do context 'when environment does not exist' do it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -161,7 +161,7 @@ describe 'projects/builds/show', :view do end end - context 'build that failed to deploy and environment has not been created' do + context 'job that failed to deploy and environment has not been created' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -171,7 +171,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed' + expected_text = 'The deployment of this job to staging did not succeed' render expect(rendered).to have_css( @@ -179,7 +179,7 @@ describe 'projects/builds/show', :view do end end - context 'build that will deploy and environment has not been created' do + context 'job that will deploy and environment has not been created' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -189,7 +189,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -200,7 +200,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is running' do + context 'when job is running' do before do build.run! render @@ -211,7 +211,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is not running' do + context 'when job is not running' do before do build.success! render diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index e471a68a49a..5ef8cf1105b 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -107,7 +107,8 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit], ) - project.repository.update_ref!( + GitOperationService.new(nil, project.repository).send( + :update_ref, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, Gitlab::Git::BLANK_SHA diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 984acdade36..5919b99a6ed 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -74,7 +74,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do - expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) + expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) end @@ -89,7 +89,7 @@ describe PostReceive do end it "asks the project to trigger all hooks" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice @@ -97,7 +97,7 @@ describe PostReceive do end it "enqueues a UpdateMergeRequestsWorker job" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) PostReceive.new.perform(pwd(project), key_id, base64_changes) diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1b910d9b91e..1f4c39eb64a 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -8,14 +8,14 @@ describe ProjectDestroyWorker do describe "#perform" do it "deletes the project" do - subject.perform(project.id, project.owner, {}) + subject.perform(project.id, project.owner.id, {}) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey end it "deletes the project but skips repo deletion" do - subject.perform(project.id, project.owner, { "skip_repo" => true }) + subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js index f5dc4abcd80..2c9b4825443 100644 --- a/vendor/assets/javascripts/date.format.js +++ b/vendor/assets/javascripts/date.format.js @@ -11,115 +11,122 @@ * The date defaults to the current date/time. * The mask defaults to dateFormat.masks.default. */ + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.dateFormat = factory()); + }(this, (function () { 'use strict'; + var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; -var dateFormat = function () { - var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, - timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, - timezoneClip = /[^-+\dA-Z]/g, - pad = function (val, len) { - val = String(val); - len = len || 2; - while (val.length < len) val = "0" + val; - return val; - }; + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; - // Regexes and supporting functions are cached through closure - return function (date, mask, utc) { - var dF = dateFormat; + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } - // You can't provide utc if you skip other args (use the "UTC:" mask prefix) - if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { - mask = date; - date = undefined; - } + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); - // Passing date through Date applies Date.parse, if necessary - date = date ? new Date(date) : new Date; - if (isNaN(date)) throw SyntaxError("invalid date"); + mask = String(dF.masks[mask] || mask || dF.masks["default"]); - mask = String(dF.masks[mask] || mask || dF.masks["default"]); + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } - // Allow setting the utc argument via the mask - if (mask.slice(0, 4) == "UTC:") { - mask = mask.slice(4); - utc = true; - } + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; - var _ = utc ? "getUTC" : "get", - d = date[_ + "Date"](), - D = date[_ + "Day"](), - m = date[_ + "Month"](), - y = date[_ + "FullYear"](), - H = date[_ + "Hours"](), - M = date[_ + "Minutes"](), - s = date[_ + "Seconds"](), - L = date[_ + "Milliseconds"](), - o = utc ? 0 : date.getTimezoneOffset(), - flags = { - d: d, - dd: pad(d), - ddd: dF.i18n.dayNames[D], - dddd: dF.i18n.dayNames[D + 7], - m: m + 1, - mm: pad(m + 1), - mmm: dF.i18n.monthNames[m], - mmmm: dF.i18n.monthNames[m + 12], - yy: String(y).slice(2), - yyyy: y, - h: H % 12 || 12, - hh: pad(H % 12 || 12), - H: H, - HH: pad(H), - M: M, - MM: pad(M), - s: s, - ss: pad(s), - l: pad(L, 3), - L: pad(L > 99 ? Math.round(L / 10) : L), - t: H < 12 ? "a" : "p", - tt: H < 12 ? "am" : "pm", - T: H < 12 ? "A" : "P", - TT: H < 12 ? "AM" : "PM", - Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), - o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), - S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] - }; + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; + }(); - return mask.replace(token, function ($0) { - return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); - }); + // Some common format strings + dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; -}(); -// Some common format strings -dateFormat.masks = { - "default": "ddd mmm dd yyyy HH:MM:ss", - shortDate: "m/d/yy", - mediumDate: "mmm d, yyyy", - longDate: "mmmm d, yyyy", - fullDate: "dddd, mmmm d, yyyy", - shortTime: "h:MM TT", - mediumTime: "h:MM:ss TT", - longTime: "h:MM:ss TT Z", - isoDate: "yyyy-mm-dd", - isoTime: "HH:MM:ss", - isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", - isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" -}; + // Internationalization strings + dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] + }; -// Internationalization strings -dateFormat.i18n = { - dayNames: [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" - ], - monthNames: [ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - ] -}; + // For convenience... + Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); + }; -// For convenience... -Date.prototype.format = function (mask, utc) { - return dateFormat(this, mask, utc); -}; + return dateFormat; +}))); diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js index 19e6c13a655..b8887115a37 100644 --- a/vendor/assets/javascripts/es6-promise.auto.js +++ b/vendor/assets/javascripts/es6-promise.auto.js @@ -1154,6 +1154,3 @@ Promise.Promise = Promise; return Promise; }))); - -ES6Promise.polyfill(); -//# sourceMappingURL=es6-promise.auto.map diff --git a/vendor/assets/javascripts/jquery.atwho.js b/vendor/assets/javascripts/jquery.atwho.js new file mode 100644 index 00000000000..0d295ebe5af --- /dev/null +++ b/vendor/assets/javascripts/jquery.atwho.js @@ -0,0 +1,1202 @@ +/** + * at.js - 1.5.1 + * Copyright (c) 2016 chord.luo <chord.luo@gmail.com>; + * Homepage: http://ichord.github.com/At.js + * License: MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { +var DEFAULT_CALLBACKS, KEY_CODE; + +KEY_CODE = { + DOWN: 40, + UP: 38, + ESC: 27, + TAB: 9, + ENTER: 13, + CTRL: 17, + A: 65, + P: 80, + N: 78, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + BACKSPACE: 8, + SPACE: 32 +}; + +DEFAULT_CALLBACKS = { + beforeSave: function(data) { + return Controller.arrayToDefaultHash(data); + }, + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var _a, _y, match, regexp, space; + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + if (should_startWithSpace) { + flag = '(?:^|\\s)' + flag; + } + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + space = acceptSpaceBar ? "\ " : ""; + regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); + match = regexp.exec(subtext); + if (match) { + return match[2] || match[1]; + } else { + return null; + } + }, + filter: function(query, data, searchKey) { + var _results, i, item, len; + _results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) { + _results.push(item); + } + } + return _results; + }, + remoteFilter: null, + sorter: function(query, items, searchKey) { + var _results, i, item, len; + if (!query) { + return items; + } + _results = []; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()); + if (item.atwho_order > -1) { + _results.push(item); + } + } + return _results.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + }, + tplEval: function(tpl, map) { + var error, error1, template; + template = tpl; + try { + if (typeof tpl !== 'string') { + template = tpl(map); + } + return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { + return map[key]; + }); + } catch (error1) { + error = error1; + return ""; + } + }, + highlighter: function(li, query) { + var regexp; + if (!query) { + return li; + } + regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); + return li.replace(regexp, function(str, $1, $2, $3) { + return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <'; + }); + }, + beforeInsert: function(value, $li, e) { + return value; + }, + beforeReposition: function(offset) { + return offset; + }, + afterMatchFailed: function(at, el) {} +}; + +var App; + +App = (function() { + function App(inputor) { + this.currentFlag = null; + this.controllers = {}; + this.aliasMaps = {}; + this.$inputor = $(inputor); + this.setupRootElement(); + this.listen(); + } + + App.prototype.createContainer = function(doc) { + var ref; + if ((ref = this.$el) != null) { + ref.remove(); + } + return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>")); + }; + + App.prototype.setupRootElement = function(iframe, asRoot) { + var error, error1; + if (asRoot == null) { + asRoot = false; + } + if (iframe) { + this.window = iframe.contentWindow; + this.document = iframe.contentDocument || this.window.document; + this.iframe = iframe; + } else { + this.document = this.$inputor[0].ownerDocument; + this.window = this.document.defaultView || this.document.parentWindow; + try { + this.iframe = this.window.frameElement; + } catch (error1) { + error = error1; + this.iframe = null; + if ($.fn.atwho.debug) { + throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error); + } + } + } + return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document); + }; + + App.prototype.controller = function(at) { + var c, current, currentFlag, ref; + if (this.aliasMaps[at]) { + current = this.controllers[this.aliasMaps[at]]; + } else { + ref = this.controllers; + for (currentFlag in ref) { + c = ref[currentFlag]; + if (currentFlag === at) { + current = c; + break; + } + } + } + if (current) { + return current; + } else { + return this.controllers[this.currentFlag]; + } + }; + + App.prototype.setContextFor = function(at) { + this.currentFlag = at; + return this; + }; + + App.prototype.reg = function(flag, setting) { + var base, controller; + controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag)); + if (setting.alias) { + this.aliasMaps[setting.alias] = flag; + } + controller.init(setting); + return this; + }; + + App.prototype.listen = function() { + return this.$inputor.on('compositionstart', (function(_this) { + return function(e) { + var ref; + if ((ref = _this.controller()) != null) { + ref.view.hide(); + } + _this.isComposing = true; + return null; + }; + })(this)).on('compositionend', (function(_this) { + return function(e) { + _this.isComposing = false; + setTimeout(function(e) { + return _this.dispatch(e); + }); + return null; + }; + })(this)).on('keyup.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeyup(e); + }; + })(this)).on('keydown.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeydown(e); + }; + })(this)).on('blur.atwhoInner', (function(_this) { + return function(e) { + var c; + if (c = _this.controller()) { + c.expectedQueryCBId = null; + return c.view.hide(e, c.getOpt("displayTimeout")); + } + }; + })(this)).on('click.atwhoInner', (function(_this) { + return function(e) { + return _this.dispatch(e); + }; + })(this)).on('scroll.atwhoInner', (function(_this) { + return function() { + var lastScrollTop; + lastScrollTop = _this.$inputor.scrollTop(); + return function(e) { + var currentScrollTop, ref; + currentScrollTop = e.target.scrollTop; + if (lastScrollTop !== currentScrollTop) { + if ((ref = _this.controller()) != null) { + ref.view.hide(e); + } + } + lastScrollTop = currentScrollTop; + return true; + }; + }; + })(this)()); + }; + + App.prototype.shutdown = function() { + var _, c, ref; + ref = this.controllers; + for (_ in ref) { + c = ref[_]; + c.destroy(); + delete this.controllers[_]; + } + this.$inputor.off('.atwhoInner'); + return this.$el.remove(); + }; + + App.prototype.dispatch = function(e) { + var _, c, ref, results; + ref = this.controllers; + results = []; + for (_ in ref) { + c = ref[_]; + results.push(c.lookUp(e)); + } + return results; + }; + + App.prototype.onKeyup = function(e) { + var ref; + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + if ((ref = this.controller()) != null) { + ref.view.hide(); + } + break; + case KEY_CODE.DOWN: + case KEY_CODE.UP: + case KEY_CODE.CTRL: + case KEY_CODE.ENTER: + $.noop(); + break; + case KEY_CODE.P: + case KEY_CODE.N: + if (!e.ctrlKey) { + this.dispatch(e); + } + break; + default: + this.dispatch(e); + } + }; + + App.prototype.onKeydown = function(e) { + var ref, view; + view = (ref = this.controller()) != null ? ref.view : void 0; + if (!(view && view.visible())) { + return; + } + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + view.hide(e); + break; + case KEY_CODE.UP: + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.DOWN: + e.preventDefault(); + view.next(); + break; + case KEY_CODE.P: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.N: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.next(); + break; + case KEY_CODE.TAB: + case KEY_CODE.ENTER: + case KEY_CODE.SPACE: + if (!view.visible()) { + return; + } + if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) { + return; + } + if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) { + return; + } + if (view.highlighted()) { + e.preventDefault(); + view.choose(e); + } else { + view.hide(e); + } + break; + default: + $.noop(); + } + }; + + return App; + +})(); + +var Controller, + slice = [].slice; + +Controller = (function() { + Controller.prototype.uid = function() { + return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); + }; + + function Controller(app, at1) { + this.app = app; + this.at = at1; + this.$inputor = this.app.$inputor; + this.id = this.$inputor[0].id || this.uid(); + this.expectedQueryCBId = null; + this.setting = null; + this.query = null; + this.pos = 0; + this.range = null; + if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { + this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>")); + } + this.model = new Model(this); + this.view = new View(this); + } + + Controller.prototype.init = function(setting) { + this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); + this.view.init(); + return this.model.reload(this.setting.data); + }; + + Controller.prototype.destroy = function() { + this.trigger('beforeDestroy'); + this.model.destroy(); + this.view.destroy(); + return this.$el.remove(); + }; + + Controller.prototype.callDefault = function() { + var args, error, error1, funcName; + funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + try { + return DEFAULT_CALLBACKS[funcName].apply(this, args); + } catch (error1) { + error = error1; + return $.error(error + " Or maybe At.js doesn't have function " + funcName); + } + }; + + Controller.prototype.trigger = function(name, data) { + var alias, eventName; + if (data == null) { + data = []; + } + data.push(this); + alias = this.getOpt('alias'); + eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho"; + return this.$inputor.trigger(eventName, data); + }; + + Controller.prototype.callbacks = function(funcName) { + return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]; + }; + + Controller.prototype.getOpt = function(at, default_value) { + var e, error1; + try { + return this.setting[at]; + } catch (error1) { + e = error1; + return null; + } + }; + + Controller.prototype.insertContentFor = function($li) { + var data, tpl; + tpl = this.getOpt('insertTpl'); + data = $.extend({}, $li.data('item-data'), { + 'atwho-at': this.at + }); + return this.callbacks("tplEval").call(this, tpl, data, "onInsert"); + }; + + Controller.prototype.renderView = function(data) { + var searchKey; + searchKey = this.getOpt("searchKey"); + data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey); + return this.view.render(data.slice(0, this.getOpt('limit'))); + }; + + Controller.arrayToDefaultHash = function(data) { + var i, item, len, results; + if (!$.isArray(data)) { + return data; + } + results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if ($.isPlainObject(item)) { + results.push(item); + } else { + results.push({ + name: item + }); + } + } + return results; + }; + + Controller.prototype.lookUp = function(e) { + var query, wait; + if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) { + return; + } + if (this.getOpt('suspendOnComposing') && this.app.isComposing) { + return; + } + query = this.catchQuery(e); + if (!query) { + this.expectedQueryCBId = null; + return query; + } + this.app.setContextFor(this.at); + if (wait = this.getOpt('delay')) { + this._delayLookUp(query, wait); + } else { + this._lookUp(query); + } + return query; + }; + + Controller.prototype._delayLookUp = function(query, wait) { + var now, remaining; + now = Date.now ? Date.now() : new Date().getTime(); + this.previousCallTime || (this.previousCallTime = now); + remaining = wait - (now - this.previousCallTime); + if ((0 < remaining && remaining < wait)) { + this.previousCallTime = now; + this._stopDelayedCall(); + return this.delayedCallTimeout = setTimeout((function(_this) { + return function() { + _this.previousCallTime = 0; + _this.delayedCallTimeout = null; + return _this._lookUp(query); + }; + })(this), wait); + } else { + this._stopDelayedCall(); + if (this.previousCallTime !== now) { + this.previousCallTime = 0; + } + return this._lookUp(query); + } + }; + + Controller.prototype._stopDelayedCall = function() { + if (this.delayedCallTimeout) { + clearTimeout(this.delayedCallTimeout); + return this.delayedCallTimeout = null; + } + }; + + Controller.prototype._generateQueryCBId = function() { + return {}; + }; + + Controller.prototype._lookUp = function(query) { + var _callback; + _callback = function(queryCBId, data) { + if (queryCBId !== this.expectedQueryCBId) { + return; + } + if (data && data.length > 0) { + return this.renderView(this.constructor.arrayToDefaultHash(data)); + } else { + return this.view.hide(); + } + }; + this.expectedQueryCBId = this._generateQueryCBId(); + return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId)); + }; + + return Controller; + +})(); + +var TextareaController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +TextareaController = (function(superClass) { + extend(TextareaController, superClass); + + function TextareaController() { + return TextareaController.__super__.constructor.apply(this, arguments); + } + + TextareaController.prototype.catchQuery = function() { + var caretPos, content, end, isString, query, start, subtext; + content = this.$inputor.val(); + caretPos = this.$inputor.caret('pos', { + iframe: this.app.iframe + }); + subtext = content.slice(0, caretPos); + query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof query === 'string'; + if (isString && query.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && query.length <= this.getOpt('maxLen', 20)) { + start = caretPos - query.length; + end = start + query.length; + this.pos = start; + query = { + 'text': query, + 'headPos': start, + 'endPos': end + }; + this.trigger("matched", [this.at, query.text]); + } else { + query = null; + this.view.hide(); + } + return this.query = query; + }; + + TextareaController.prototype.rect = function() { + var c, iframeOffset, scaleBottom; + if (!(c = this.$inputor.caret('offset', this.pos - 1, { + iframe: this.app.iframe + }))) { + return; + } + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = $(this.app.iframe).offset(); + c.left += iframeOffset.left; + c.top += iframeOffset.top; + } + scaleBottom = this.app.document.selection ? 0 : 2; + return { + left: c.left, + top: c.top, + bottom: c.top + c.height + scaleBottom + }; + }; + + TextareaController.prototype.insert = function(content, $li) { + var $inputor, source, startStr, suffix, text; + $inputor = this.$inputor; + source = $inputor.val(); + startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0)); + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " "; + content += suffix; + text = "" + startStr + content + (source.slice(this.query['endPos'] || 0)); + $inputor.val(text); + $inputor.caret('pos', startStr.length + content.length, { + iframe: this.app.iframe + }); + if (!$inputor.is(':focus')) { + $inputor.focus(); + } + return $inputor.change(); + }; + + return TextareaController; + +})(Controller); + +var EditableController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +EditableController = (function(superClass) { + extend(EditableController, superClass); + + function EditableController() { + return EditableController.__super__.constructor.apply(this, arguments); + } + + EditableController.prototype._getRange = function() { + var sel; + sel = this.app.window.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } + }; + + EditableController.prototype._setRange = function(position, node, range) { + if (range == null) { + range = this._getRange(); + } + if (!range) { + return; + } + node = $(node)[0]; + if (position === 'after') { + range.setEndAfter(node); + range.setStartAfter(node); + } else { + range.setEndBefore(node); + range.setStartBefore(node); + } + range.collapse(false); + return this._clearRange(range); + }; + + EditableController.prototype._clearRange = function(range) { + var sel; + if (range == null) { + range = this._getRange(); + } + sel = this.app.window.getSelection(); + if (this.ctrl_a_pressed == null) { + sel.removeAllRanges(); + return sel.addRange(range); + } + }; + + EditableController.prototype._movingEvent = function(e) { + var ref; + return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN); + }; + + EditableController.prototype._unwrap = function(node) { + var next; + node = $(node).unwrap().get(0); + if ((next = node.nextSibling) && next.nodeValue) { + node.nodeValue += next.nodeValue; + $(next).remove(); + } + return node; + }; + + EditableController.prototype.catchQuery = function(e) { + var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range; + if (!(range = this._getRange())) { + return; + } + if (!range.collapsed) { + return; + } + if (e.which === KEY_CODE.ENTER) { + ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap(); + if ($query.is(':empty')) { + $query.remove(); + } + ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap(); + this._clearRange(); + return; + } + if (/firefox/i.test(navigator.userAgent)) { + if ($(range.startContainer).is(this.$inputor)) { + this._clearRange(); + return; + } + if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) { + _range = range.cloneRange(); + _range.setStart(range.startContainer, offset); + if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) { + inserted = $(range.startContainer).contents().get(offset); + this._setRange('after', $(inserted).contents().last()); + } + } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) { + $inserted = $(range.startContainer.previousSibling); + if ($inserted.is('.atwho-inserted') && range.startOffset === 0) { + this._setRange('after', $inserted.contents().last()); + } + } + } + $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query'); + if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) { + $query.remove(); + } + if (!this._movingEvent(e)) { + $query.removeClass('atwho-inserted'); + } + if ($query.length > 0) { + switch (e.which) { + case KEY_CODE.LEFT: + this._setRange('before', $query.get(0), range); + $query.removeClass('atwho-query'); + return; + case KEY_CODE.RIGHT: + this._setRange('after', $query.get(0).nextSibling, range); + $query.removeClass('atwho-query'); + return; + } + } + if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) { + $query.empty().html(query_content).attr('data-atwho-at-query', null); + this._setRange('after', $query.get(0), range); + } + _range = range.cloneRange(); + _range.setStart(range.startContainer, 0); + matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof matched === 'string'; + if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) { + range.setStart(range.startContainer, index); + $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query'); + range.surroundContents($query.get(0)); + lastNode = $query.contents().last().get(0); + if (/firefox/i.test(navigator.userAgent)) { + range.setStart(lastNode, lastNode.length); + range.setEnd(lastNode, lastNode.length); + this._clearRange(range); + } else { + this._setRange('after', lastNode, range); + } + } + if (isString && matched.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && matched.length <= this.getOpt('maxLen', 20)) { + query = { + text: matched, + el: $query + }; + this.trigger("matched", [this.at, query.text]); + return this.query = query; + } else { + this.view.hide(); + this.query = { + el: $query + }; + if ($query.text().indexOf(this.at) >= 0) { + if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) { + $query.removeClass('atwho-query'); + } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) { + this._setRange("after", this._unwrap($query.text($query.text()).contents().first())); + } + } + return null; + } + }; + + EditableController.prototype.rect = function() { + var $iframe, iframeOffset, rect; + rect = this.query.el.offset(); + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = ($iframe = $(this.app.iframe)).offset(); + rect.left += iframeOffset.left - this.$inputor.scrollLeft(); + rect.top += iframeOffset.top - this.$inputor.scrollTop(); + } + rect.bottom = rect.top + this.query.el.height(); + return rect; + }; + + EditableController.prototype.insert = function(content, $li) { + var data, range, suffix, suffixNode; + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0"; + data = $li.data('item-data'); + this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text); + if (range = this._getRange()) { + range.setEndAfter(this.query.el[0]); + range.collapse(false); + range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix)); + this._setRange('after', suffixNode, range); + } + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + return this.$inputor.change(); + }; + + return EditableController; + +})(Controller); + +var Model; + +Model = (function() { + function Model(context) { + this.context = context; + this.at = this.context.at; + this.storage = this.context.$inputor; + } + + Model.prototype.destroy = function() { + return this.storage.data(this.at, null); + }; + + Model.prototype.saved = function() { + return this.fetch() > 0; + }; + + Model.prototype.query = function(query, callback) { + var _remoteFilter, data, searchKey; + data = this.fetch(); + searchKey = this.context.getOpt("searchKey"); + data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || []; + _remoteFilter = this.context.callbacks('remoteFilter'); + if (data.length > 0 || (!_remoteFilter && data.length === 0)) { + return callback(data); + } else { + return _remoteFilter.call(this.context, query, callback); + } + }; + + Model.prototype.fetch = function() { + return this.storage.data(this.at) || []; + }; + + Model.prototype.save = function(data) { + return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || [])); + }; + + Model.prototype.load = function(data) { + if (!(this.saved() || !data)) { + return this._load(data); + } + }; + + Model.prototype.reload = function(data) { + return this._load(data); + }; + + Model.prototype._load = function(data) { + if (typeof data === "string") { + return $.ajax(data, { + dataType: "json" + }).done((function(_this) { + return function(data) { + return _this.save(data); + }; + })(this)); + } else { + return this.save(data); + } + }; + + return Model; + +})(); + +var View; + +View = (function() { + function View(context) { + this.context = context; + this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>"); + this.$elUl = this.$el.children(); + this.timeoutID = null; + this.context.$el.append(this.$el); + this.bindEvent(); + } + + View.prototype.init = function() { + var header_tpl, id; + id = this.context.getOpt("alias") || this.context.at.charCodeAt(0); + header_tpl = this.context.getOpt("headerTpl"); + if (header_tpl && this.$el.children().length === 1) { + this.$el.prepend(header_tpl); + } + return this.$el.attr({ + 'id': "at-view-" + id + }); + }; + + View.prototype.destroy = function() { + return this.$el.remove(); + }; + + View.prototype.bindEvent = function() { + var $menu, lastCoordX, lastCoordY; + $menu = this.$el.find('ul'); + lastCoordX = 0; + lastCoordY = 0; + return $menu.on('mousemove.atwho-view', 'li', (function(_this) { + return function(e) { + var $cur; + if (lastCoordX === e.clientX && lastCoordY === e.clientY) { + return; + } + lastCoordX = e.clientX; + lastCoordY = e.clientY; + $cur = $(e.currentTarget); + if ($cur.hasClass('cur')) { + return; + } + $menu.find('.cur').removeClass('cur'); + return $cur.addClass('cur'); + }; + })(this)).on('click.atwho-view', 'li', (function(_this) { + return function(e) { + $menu.find('.cur').removeClass('cur'); + $(e.currentTarget).addClass('cur'); + _this.choose(e); + return e.preventDefault(); + }; + })(this)); + }; + + View.prototype.visible = function() { + return this.$el.is(":visible"); + }; + + View.prototype.highlighted = function() { + return this.$el.find(".cur").length > 0; + }; + + View.prototype.choose = function(e) { + var $li, content; + if (($li = this.$el.find(".cur")).length) { + content = this.context.insertContentFor($li); + this.context._stopDelayedCall(); + this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li); + this.context.trigger("inserted", [$li, e]); + this.hide(e); + } + if (this.context.getOpt("hideWithoutSuffix")) { + return this.stopShowing = true; + } + }; + + View.prototype.reposition = function(rect) { + var _window, offset, overflowOffset, ref; + _window = this.context.app.iframeAsRoot ? this.context.app.window : window; + if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { + rect.bottom = rect.top - this.$el.height(); + } + if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { + rect.left = overflowOffset; + } + offset = { + left: rect.left, + top: rect.bottom + }; + if ((ref = this.context.callbacks("beforeReposition")) != null) { + ref.call(this.context, offset); + } + this.$el.offset(offset); + return this.context.trigger("reposition", [offset]); + }; + + View.prototype.next = function() { + var cur, next, nextEl, offset; + cur = this.$el.find('.cur').removeClass('cur'); + next = cur.next(); + if (!next.length) { + next = this.$el.find('li:first'); + } + next.addClass('cur'); + nextEl = next[0]; + offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.prev = function() { + var cur, offset, prev, prevEl; + cur = this.$el.find('.cur').removeClass('cur'); + prev = cur.prev(); + if (!prev.length) { + prev = this.$el.find('li:last'); + } + prev.addClass('cur'); + prevEl = prev[0]; + offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.scrollTop = function(scrollTop) { + var scrollDuration; + scrollDuration = this.context.getOpt('scrollDuration'); + if (scrollDuration) { + return this.$elUl.animate({ + scrollTop: scrollTop + }, scrollDuration); + } else { + return this.$elUl.scrollTop(scrollTop); + } + }; + + View.prototype.show = function() { + var rect; + if (this.stopShowing) { + this.stopShowing = false; + return; + } + if (!this.visible()) { + this.$el.show(); + this.$el.scrollTop(0); + this.context.trigger('shown'); + } + if (rect = this.context.rect()) { + return this.reposition(rect); + } + }; + + View.prototype.hide = function(e, time) { + var callback; + if (!this.visible()) { + return; + } + if (isNaN(time)) { + this.$el.hide(); + return this.context.trigger('hidden', [e]); + } else { + callback = (function(_this) { + return function() { + return _this.hide(); + }; + })(this); + clearTimeout(this.timeoutID); + return this.timeoutID = setTimeout(callback, time); + } + }; + + View.prototype.render = function(list) { + var $li, $ul, i, item, len, li, tpl; + if (!($.isArray(list) && list.length > 0)) { + this.hide(); + return; + } + this.$el.find('ul').empty(); + $ul = this.$el.find('ul'); + tpl = this.context.getOpt('displayTpl'); + for (i = 0, len = list.length; i < len; i++) { + item = list[i]; + item = $.extend({}, item, { + 'atwho-at': this.context.at + }); + li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay"); + $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); + $li.data("item-data", item); + $ul.append($li); + } + this.show(); + if (this.context.getOpt('highlightFirst')) { + return $ul.find("li:first").addClass("cur"); + } + }; + + return View; + +})(); + +var Api; + +Api = { + load: function(at, data) { + var c; + if (c = this.controller(at)) { + return c.model.load(data); + } + }, + isSelecting: function() { + var ref; + return !!((ref = this.controller()) != null ? ref.view.visible() : void 0); + }, + hide: function() { + var ref; + return (ref = this.controller()) != null ? ref.view.hide() : void 0; + }, + reposition: function() { + var c; + if (c = this.controller()) { + return c.view.reposition(c.rect()); + } + }, + setIframe: function(iframe, asRoot) { + this.setupRootElement(iframe, asRoot); + return null; + }, + run: function() { + return this.dispatch(); + }, + destroy: function() { + this.shutdown(); + return this.$inputor.data('atwho', null); + } +}; + +$.fn.atwho = function(method) { + var _args, result; + _args = arguments; + result = null; + this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { + var $this, app; + if (!(app = ($this = $(this)).data("atwho"))) { + $this.data('atwho', (app = new App(this))); + } + if (typeof method === 'object' || !method) { + return app.reg(method.at, method); + } else if (Api[method] && app) { + return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); + } else { + return $.error("Method " + method + " does not exist on jQuery.atwho"); + } + }); + if (result != null) { + return result; + } else { + return this; + } +}; + +$.fn.atwho["default"] = { + at: void 0, + alias: void 0, + data: null, + displayTpl: "<li>${name}</li>", + insertTpl: "${atwho-at}${name}", + headerTpl: null, + callbacks: DEFAULT_CALLBACKS, + searchKey: "name", + suffix: void 0, + hideWithoutSuffix: false, + startWithSpace: true, + acceptSpaceBar: false, + highlightFirst: true, + limit: 5, + maxLen: 20, + minLen: 0, + displayTimeout: 300, + delay: null, + spaceSelectsMatch: false, + tabSelectsMatch: true, + editableAtwhoQueryAttrs: {}, + scrollDuration: 150, + suspendOnComposing: true, + lookUpOnClick: true +}; + +$.fn.atwho.debug = false; + +})); diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js new file mode 100644 index 00000000000..811ec63ee47 --- /dev/null +++ b/vendor/assets/javascripts/jquery.caret.js @@ -0,0 +1,436 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(["jquery"], function ($) { + return (root.returnExportsGlobal = factory($)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { + +/* + Implement Github like autocomplete mentions + http://ichord.github.com/At.js + + Copyright (c) 2013 chord.luo@gmail.com + Licensed under the MIT license. +*/ + +/* +本插件操作 textarea 或者 input 内的插入符 +只实现了获得插入符在文本框中的位置,我设置 +插入符的位置. +*/ + +"use strict"; +var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy; + +pluginName = 'caret'; + +EditableCaret = (function() { + function EditableCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + EditableCaret.prototype.setPos = function(pos) { + var fn, found, offset, sel; + if (sel = oWindow.getSelection()) { + offset = 0; + found = false; + (fn = function(pos, parent) { + var node, range, _i, _len, _ref, _results; + _ref = parent.childNodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + if (found) { + break; + } + if (node.nodeType === 3) { + if (offset + node.length >= pos) { + found = true; + range = oDocument.createRange(); + range.setStart(node, pos - offset); + sel.removeAllRanges(); + sel.addRange(range); + break; + } else { + _results.push(offset += node.length); + } + } else { + _results.push(fn(pos, node)); + } + } + return _results; + })(pos, this.domInputor); + } + return this.domInputor; + }; + + EditableCaret.prototype.getIEPosition = function() { + return this.getPosition(); + }; + + EditableCaret.prototype.getPosition = function() { + var inputor_offset, offset; + offset = this.getOffset(); + inputor_offset = this.$inputor.offset(); + offset.left -= inputor_offset.left; + offset.top -= inputor_offset.top; + return offset; + }; + + EditableCaret.prototype.getOldIEPos = function() { + var preCaretTextRange, textRange; + textRange = oDocument.selection.createRange(); + preCaretTextRange = oDocument.body.createTextRange(); + preCaretTextRange.moveToElementText(this.domInputor); + preCaretTextRange.setEndPoint("EndToEnd", textRange); + return preCaretTextRange.text.length; + }; + + EditableCaret.prototype.getPos = function() { + var clonedRange, pos, range; + if (range = this.range()) { + clonedRange = range.cloneRange(); + clonedRange.selectNodeContents(this.domInputor); + clonedRange.setEnd(range.endContainer, range.endOffset); + pos = clonedRange.toString().length; + clonedRange.detach(); + return pos; + } else if (oDocument.selection) { + return this.getOldIEPos(); + } + }; + + EditableCaret.prototype.getOldIEOffset = function() { + var range, rect; + range = oDocument.selection.createRange().duplicate(); + range.moveStart("character", -1); + rect = range.getBoundingClientRect(); + return { + height: rect.bottom - rect.top, + left: rect.left, + top: rect.top + }; + }; + + EditableCaret.prototype.getOffset = function(pos) { + var clonedRange, offset, range, rect, shadowCaret; + if (oWindow.getSelection && (range = this.range())) { + if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) { + clonedRange = range.cloneRange(); + clonedRange.setStart(range.endContainer, range.endOffset - 1); + clonedRange.setEnd(range.endContainer, range.endOffset); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left + rect.width, + top: rect.top + }; + clonedRange.detach(); + } + if (!offset || (offset != null ? offset.height : void 0) === 0) { + clonedRange = range.cloneRange(); + shadowCaret = $(oDocument.createTextNode("|")); + clonedRange.insertNode(shadowCaret[0]); + clonedRange.selectNode(shadowCaret[0]); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left, + top: rect.top + }; + shadowCaret.remove(); + clonedRange.detach(); + } + } else if (oDocument.selection) { + offset = this.getOldIEOffset(); + } + if (offset) { + offset.top += $(oWindow).scrollTop(); + offset.left += $(oWindow).scrollLeft(); + } + return offset; + }; + + EditableCaret.prototype.range = function() { + var sel; + if (!oWindow.getSelection) { + return; + } + sel = oWindow.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } else { + return null; + } + }; + + return EditableCaret; + +})(); + +InputCaret = (function() { + function InputCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + InputCaret.prototype.getIEPos = function() { + var endRange, inputor, len, normalizedValue, pos, range, textInputRange; + inputor = this.domInputor; + range = oDocument.selection.createRange(); + pos = 0; + if (range && range.parentElement() === inputor) { + normalizedValue = inputor.value.replace(/\r\n/g, "\n"); + len = normalizedValue.length; + textInputRange = inputor.createTextRange(); + textInputRange.moveToBookmark(range.getBookmark()); + endRange = inputor.createTextRange(); + endRange.collapse(false); + if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { + pos = len; + } else { + pos = -textInputRange.moveStart("character", -len); + } + } + return pos; + }; + + InputCaret.prototype.getPos = function() { + if (oDocument.selection) { + return this.getIEPos(); + } else { + return this.domInputor.selectionStart; + } + }; + + InputCaret.prototype.setPos = function(pos) { + var inputor, range; + inputor = this.domInputor; + if (oDocument.selection) { + range = inputor.createTextRange(); + range.move("character", pos); + range.select(); + } else if (inputor.setSelectionRange) { + inputor.setSelectionRange(pos, pos); + } + return inputor; + }; + + InputCaret.prototype.getIEOffset = function(pos) { + var h, textRange, x, y; + textRange = this.domInputor.createTextRange(); + pos || (pos = this.getPos()); + textRange.move('character', pos); + x = textRange.boundingLeft; + y = textRange.boundingTop; + h = textRange.boundingHeight; + return { + left: x, + top: y, + height: h + }; + }; + + InputCaret.prototype.getOffset = function(pos) { + var $inputor, offset, position; + $inputor = this.$inputor; + if (oDocument.selection) { + offset = this.getIEOffset(pos); + offset.top += $(oWindow).scrollTop() + $inputor.scrollTop(); + offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft(); + return offset; + } else { + offset = $inputor.offset(); + position = this.getPosition(pos); + return offset = { + left: offset.left + position.left - $inputor.scrollLeft(), + top: offset.top + position.top - $inputor.scrollTop(), + height: position.height + }; + } + }; + + InputCaret.prototype.getPosition = function(pos) { + var $inputor, at_rect, end_range, format, html, mirror, start_range; + $inputor = this.$inputor; + format = function(value) { + value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>"); + if (/firefox/i.test(navigator.userAgent)) { + value = value.replace(/\s/g, ' '); + } + return value; + }; + if (pos === void 0) { + pos = this.getPos(); + } + start_range = $inputor.val().slice(0, pos); + end_range = $inputor.val().slice(pos); + html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>"; + html += "<span id='caret' style='position: relative; display: inline;'>|</span>"; + html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>"; + mirror = new Mirror($inputor); + return at_rect = mirror.create(html).rect(); + }; + + InputCaret.prototype.getIEPosition = function(pos) { + var h, inputorOffset, offset, x, y; + offset = this.getIEOffset(pos); + inputorOffset = this.$inputor.offset(); + x = offset.left - inputorOffset.left; + y = offset.top - inputorOffset.top; + h = offset.height; + return { + left: x, + top: y, + height: h + }; + }; + + return InputCaret; + +})(); + +Mirror = (function() { + Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"]; + + function Mirror($inputor) { + this.$inputor = $inputor; + } + + Mirror.prototype.mirrorCss = function() { + var css, + _this = this; + css = { + position: 'absolute', + left: -9999, + top: 0, + zIndex: -20000 + }; + if (this.$inputor.prop('tagName') === 'TEXTAREA') { + this.css_attr.push('width'); + } + $.each(this.css_attr, function(i, p) { + return css[p] = _this.$inputor.css(p); + }); + return css; + }; + + Mirror.prototype.create = function(html) { + this.$mirror = $('<div></div>'); + this.$mirror.css(this.mirrorCss()); + this.$mirror.html(html); + this.$inputor.after(this.$mirror); + return this; + }; + + Mirror.prototype.rect = function() { + var $flag, pos, rect; + $flag = this.$mirror.find("#caret"); + pos = $flag.position(); + rect = { + left: pos.left, + top: pos.top, + height: $flag.height() + }; + this.$mirror.remove(); + return rect; + }; + + return Mirror; + +})(); + +Utils = { + contentEditable: function($inputor) { + return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true'); + } +}; + +methods = { + pos: function(pos) { + if (pos || pos === 0) { + return this.setPos(pos); + } else { + return this.getPos(); + } + }, + position: function(pos) { + if (oDocument.selection) { + return this.getIEPosition(pos); + } else { + return this.getPosition(pos); + } + }, + offset: function(pos) { + var offset; + offset = this.getOffset(pos); + return offset; + } +}; + +oDocument = null; + +oWindow = null; + +oFrame = null; + +setContextBy = function(settings) { + var iframe; + if (iframe = settings != null ? settings.iframe : void 0) { + oFrame = iframe; + oWindow = iframe.contentWindow; + return oDocument = iframe.contentDocument || oWindow.document; + } else { + oFrame = void 0; + oWindow = window; + return oDocument = document; + } +}; + +discoveryIframeOf = function($dom) { + var error; + oDocument = $dom[0].ownerDocument; + oWindow = oDocument.defaultView || oDocument.parentWindow; + try { + return oFrame = oWindow.frameElement; + } catch (_error) { + error = _error; + } +}; + +$.fn.caret = function(method, value, settings) { + var caret; + if (methods[method]) { + if ($.isPlainObject(value)) { + setContextBy(value); + value = void 0; + } else { + setContextBy(settings); + } + caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this); + return methods[method].apply(caret, [value]); + } else { + return $.error("Method " + method + " does not exist on jQuery.caret"); + } +}; + +$.fn.caret.EditableCaret = EditableCaret; + +$.fn.caret.InputCaret = InputCaret; + +$.fn.caret.Utils = Utils; + +$.fn.caret.apis = methods; + + +})); diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js deleted file mode 100644 index fd6e95e75d5..00000000000 --- a/vendor/assets/javascripts/jquery.turbolinks.js +++ /dev/null @@ -1,49 +0,0 @@ -// Generated by CoffeeScript 1.7.1 - -/* -jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks -jQuery plugin for drop-in fix binded events problem caused by Turbolinks - -The MIT License -Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz - */ - -(function() { - var $, $document; - - $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); - - $document = $(document); - - $.turbo = { - version: '2.1.0', - isReady: false, - use: function(load, fetch) { - return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); - }, - addCallback: function(callback) { - if ($.turbo.isReady) { - callback($); - } - return $document.on('turbo:ready', function() { - return callback($); - }); - }, - onLoad: function() { - $.turbo.isReady = true; - return $document.trigger('turbo:ready'); - }, - onFetch: function() { - return $.turbo.isReady = false; - }, - register: function() { - $(this.onLoad); - return $.fn.ready = this.addCallback; - } - }; - - $.turbo.register(); - - $.turbo.use('page:load', 'page:fetch'); - -}).call(this); diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js index e666b136051..a33e5e0ade9 100644 --- a/vendor/assets/javascripts/u2f.js +++ b/vendor/assets/javascripts/u2f.js @@ -745,4 +745,6 @@ u2f.getApiVersion = function(callback, opt_timeoutSeconds) { }; port.postMessage(req); }); -};
\ No newline at end of file +}; + +window.u2f || (window.u2f = u2f); diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js index 7e24fd9b36e..55438452cad 100644 --- a/vendor/assets/javascripts/xterm/fit.js +++ b/vendor/assets/javascripts/xterm/fit.js @@ -16,12 +16,12 @@ /* * CommonJS environment */ - module.exports = fit(require('../../xterm')); + module.exports = fit(require('./xterm')); } else if (typeof define == 'function') { /* * Require.js is available */ - define(['../../xterm'], fit); + define(['./xterm'], fit); } else { /* * Plain browser environment |