diff options
author | blackst0ne <blackst0ne.ru@gmail.com> | 2017-02-21 12:15:58 +1100 |
---|---|---|
committer | blackst0ne <blackst0ne.ru@gmail.com> | 2017-02-21 12:15:58 +1100 |
commit | 9a9a368fe0bb0a5983794ab272c2a12259d77939 (patch) | |
tree | b4f6293174c9bf9df2ae0ff8e63f6928fadb046e | |
parent | 2a12cbe6d6d5c7c78c6fac64e7d5a6af6742462a (diff) | |
parent | 510c51a6b5fd822755046e10f5c5d875ad5fba9f (diff) | |
download | gitlab-ce-9a9a368fe0bb0a5983794ab272c2a12259d77939.tar.gz |
Merge remote-tracking branch 'upstream/master' into add_mr_info_to_issues_list
489 files changed, 4704 insertions, 2692 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8619d7d3f8..433b3119fba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -240,7 +240,7 @@ rake db:seed_fu: paths: - log/development.log -karma: +rake karma: cache: paths: - vendor/ruby @@ -387,7 +387,7 @@ pages: <<: *dedicated-runner dependencies: - coverage - - karma + - rake karma - lint:javascript:report script: - mv public/ .public/ diff --git a/.gitlab/issue_templates/Research Proposal.md b/.gitlab/issue_templates/Research Proposal.md new file mode 100644 index 00000000000..5676656793d --- /dev/null +++ b/.gitlab/issue_templates/Research Proposal.md @@ -0,0 +1,17 @@ +### Background: + +(Include problem, use cases, benefits, and/or goals) + +**What questions are you trying to answer?** + +**Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?** + +**What is the backstory of this project and how does it impact the approach?** + +**What do you already know about the areas you are exploring?** + +**What does success look like at the end of the project?** + +### Links / references: + +/label ~"UX research" diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaac0f69d3..58b8cf2ad83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.6 (2017-02-17) + +- API: Fix file downloading. !0 (8267) +- Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms. !8752 +- Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory. !8891 +- Fix wrong call to ProjectCacheWorker.perform. !8910 +- Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index. !8956 +- Fix broken anchor links when special characters are used. !8961 (Andrey Krivko) +- Do not display deploy keys in user's own ssh keys list. !9024 +- Show merge errors in merge request widget. !9229 +- Don't delete assigned MRs/issues when user is deleted. +- backport of EE fix !954. +- Refresh authorizations when transferring projects. +- Don't use backup Active Record connections for Sidekiq. +- Check public snippets for spam. + ## 8.16.5 (2017-02-14) - Patch Asciidocs rendering to block XSS. @@ -181,6 +197,10 @@ entry. - Add margin to markdown math blocks. - Add hover state to MR comment reply button. +## 8.15.7 (2017-02-15) + +- No changes. + ## 8.15.6 (2017-02-14) - Patch Asciidocs rendering to block XSS. @@ -451,6 +471,10 @@ entry. - Whitelist next project names: help, ci, admin, search. !8227 - Adds back CSS for progress-bars. !8237 +## 8.14.10 (2017-02-15) + +- No changes. + ## 8.14.9 (2017-02-14) - Patch Asciidocs rendering to block XSS. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72cd57ad7ff..de32a953f63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,18 +93,20 @@ Please see the [UX Guide for GitLab]. ### 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]. +After each release, 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 on them. +If you're interested, you can even join the +[retrospective call][retro-kickoff-call], on the first working day after the +22nd at 6pm CET / 9am PST. ### Kickoff -Before working on the next release (usually on the 8th of each month), we have a +Before working on the next release, 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]. +[kickoff notes] are public and you are invited to comment on them. +If you're interested, you can even join the [kickoff call][retro-kickoff-call], +on the first working day after the 7th at 6pm CET / 9am PST.. [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 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 0d91a54c7d4..9e11b32fcaa 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.3.0 +0.3.1 @@ -34,7 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' -gem 'omniauth-authentiq', '~> 0.2.0' +gem 'omniauth-authentiq', '~> 0.3.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt', '~> 1.5.6' diff --git a/Gemfile.lock b/Gemfile.lock index a3c2fad41ba..4f98dc9d085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -448,7 +448,7 @@ GEM rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) - omniauth-authentiq (0.2.2) + omniauth-authentiq (0.3.0) omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) @@ -925,7 +925,7 @@ DEPENDENCIES oj (~> 2.17.4) omniauth (~> 1.3.2) omniauth-auth0 (~> 1.4.1) - omniauth-authentiq (~> 0.2.0) + omniauth-authentiq (~> 0.3.0) omniauth-azure-oauth2 (~> 0.0.6) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico Binary files differindex 71acdf670ab..156fcf07588 100644..100755 --- a/app/assets/images/favicon-blue.ico +++ b/app/assets/images/favicon-blue.ico diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 424dc719c78..aaed74d6073 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -61,4 +61,4 @@ return Admin; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 84bbe90f3b1..86e0ad89431 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -147,4 +147,4 @@ }; window.Api = Api; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4b5c9686cab..8e468faedbf 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -101,11 +101,6 @@ require('es6-promise').polyfill(); } }); - $('.nav-sidebar').niceScroll({ - cursoropacitymax: '0.4', - cursorcolor: '#FFF', - cursorborder: '1px solid #FFF' - }); $('.js-select-on-focus').on('focusin', function () { return $(this).select().one('mouseup', function (e) { return e.preventDefault(); @@ -245,9 +240,7 @@ require('es6-promise').polyfill(); }); gl.awardsHandler = new AwardsHandler(); new Aside(); - // bind sidebar events - new gl.Sidebar(); gl.utils.initTimeagoTimeout(); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 8438de6cdf1..448e6e2cc78 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -22,4 +22,4 @@ return Aside; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index b16a2c0f73a..e55405135fb 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -59,4 +59,4 @@ return Autosave; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 9d776b74965..a4ccb30e447 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -377,4 +377,4 @@ var emojiAliases = require('emoji-aliases'); return AwardsHandler; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a489523b802..f7f41d55b52 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -25,4 +25,4 @@ var autosize = require('vendor/autosize'); autosize.update($fields); return $fields.css('resize', 'vertical'); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 6af8f593872..fd0840fa117 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -23,4 +23,4 @@ return e.preventDefault(); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 7747306688c..a7e68ae5cb9 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -74,4 +74,4 @@ require('../extensions/jquery'); return $this.tooltip('hide'); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 6276933e93e..6b21695d082 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -59,4 +59,4 @@ require('../extensions/jquery'); return hideOrShowHelpBlock($form); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 04bfe363929..5f14ff40eee 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -63,4 +63,4 @@ return BlobFileDropzone; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 1d0bcf6471f..de20eab9cd1 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -20,4 +20,4 @@ require('./template_selector'); return BlobGitignoreSelector; })(gl.TemplateSelector); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index 8236457f0f1..43e5c0a5641 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -23,4 +23,4 @@ return BlobGitignoreSelectors; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index 1d5672d4c48..b582052a76e 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -25,4 +25,4 @@ require('./template_selector'); return BlobLicenseSelector; })(gl.TemplateSelector); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index 9e0754819fa..0436bbb0eaf 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -12,4 +12,4 @@ require('./edit_blob'); var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); new NewCommitForm($('.js-edit-blob-form')); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 079445e8278..a1127b9e30e 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -85,4 +85,4 @@ return EditBlob; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 8f30900198e..878ad1b6031 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -95,7 +95,7 @@ $(() => { }, computed: { disabled() { - return Store.shouldAddBlankState(); + return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; }, }, template: ` diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index f8dac1ff56e..22e93328548 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -69,4 +69,4 @@ })(this)); window.Breakpoints = Breakpoints; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index dbdadc73c3f..e8531c43b4b 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -31,4 +31,4 @@ } }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c5a962dd199..8fa1aceddff 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -275,4 +275,4 @@ return Build; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 083448552b6..cae9a0ffca4 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -23,4 +23,4 @@ return BuildArtifacts; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index c656ae4e241..566b322eb49 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -11,4 +11,4 @@ return Commit; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 184b4561d2e..ee087c978dd 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -11,4 +11,4 @@ return CommitFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index f09a6b1e676..49bb64a3472 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -173,4 +173,4 @@ return ImageFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index c6fdfbcaa10..ccd895f3bf4 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -65,4 +65,4 @@ return CommitsList; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 9591df70e9c..15df105d4cc 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -88,4 +88,4 @@ return Compare; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index 3587431ab69..1eca973e069 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -66,4 +66,4 @@ return CompareAutocomplete; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 35d98492012..a1c1b721228 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -28,4 +28,4 @@ return ConfirmDangerModal; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 0029c59e550..615f485e18a 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -46,4 +46,4 @@ window.Clipboard = require('vendor/clipboard'); clipboard.on('success', genericSuccess); return clipboard.on('error', genericError); }); -}).call(this); +}).call(window); 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 dbdb01c8c68..1ac715aab77 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -97,7 +97,7 @@ $(() => { } this.isLoadingStage = true; - cycleAnalyticsStore.setStageEvents([]); + cycleAnalyticsStore.setStageEvents([], stage); cycleAnalyticsStore.setActiveStage(stage); cycleAnalyticsService @@ -107,7 +107,7 @@ $(() => { }) .done((response) => { this.isEmptyStage = !response.events.length; - cycleAnalyticsStore.setStageEvents(response.events); + cycleAnalyticsStore.setStageEvents(response.events, stage); }) .error(() => { this.isEmptyStage = true; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 index be732971c7f..3efeb141008 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js.es6 @@ -1,4 +1,8 @@ /* eslint-disable no-param-reassign */ + +require('../lib/utils/text_utility'); +const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); + ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -34,11 +38,12 @@ }); newData.stages.forEach((item) => { - const stageName = item.title.toLowerCase(); + const stageSlug = gl.text.dasherize(item.title.toLowerCase()); item.active = false; - item.isUserAllowed = data.permissions[stageName]; - item.emptyStageText = EMPTY_STAGE_TEXTS[stageName]; - item.component = `stage-${stageName}-component`; + item.isUserAllowed = data.permissions[stageSlug]; + item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug]; + item.component = `stage-${stageSlug}-component`; + item.slug = stageSlug; }); newData.analytics = data; return newData; @@ -58,31 +63,33 @@ this.deactivateAllStages(); stage.active = true; }, - setStageEvents(events) { - this.state.events = this.decorateEvents(events); + setStageEvents(events, stage) { + this.state.events = this.decorateEvents(events, stage); }, - decorateEvents(events) { + decorateEvents(events, stage) { const newEvents = []; events.forEach((item) => { if (!item) return; - item.totalTime = item.total_time; - item.author.webUrl = item.author.web_url; - item.author.avatarUrl = item.author.avatar_url; + const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); + + eventItem.totalTime = eventItem.total_time; + eventItem.author.webUrl = eventItem.author.web_url; + eventItem.author.avatarUrl = eventItem.author.avatar_url; - if (item.created_at) item.createdAt = item.created_at; - if (item.short_sha) item.shortSha = item.short_sha; - if (item.commit_url) item.commitUrl = item.commit_url; + if (eventItem.created_at) eventItem.createdAt = eventItem.created_at; + if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha; + if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url; - delete item.author.web_url; - delete item.author.avatar_url; - delete item.total_time; - delete item.created_at; - delete item.short_sha; - delete item.commit_url; + delete eventItem.author.web_url; + delete eventItem.author.avatar_url; + delete eventItem.total_time; + delete eventItem.created_at; + delete eventItem.short_sha; + delete eventItem.commit_url; - newEvents.push(item); + newEvents.push(eventItem); }); return newEvents; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 new file mode 100644 index 00000000000..cfaf9835bf8 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js.es6 @@ -0,0 +1,98 @@ +module.exports = { + issue: { + created_at: '', + url: '', + iid: '', + title: '', + total_time: {}, + author: { + avatar_url: '', + id: '', + name: '', + web_url: '', + }, + }, + plan: { + title: '', + commit_url: '', + short_sha: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + code: { + title: '', + iid: '', + created_at: '', + url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + test: { + name: '', + id: '', + date: '', + url: '', + short_sha: '', + commit_url: '', + total_time: {}, + branch: { + name: '', + url: '', + }, + }, + review: { + title: '', + iid: '', + created_at: '', + url: '', + state: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, + staging: { + id: '', + short_sha: '', + date: '', + url: '', + commit_url: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + branch: { + name: '', + url: '', + }, + }, + production: { + title: '', + created_at: '', + url: '', + iid: '', + total_time: {}, + author: { + name: '', + id: '', + avatar_url: '', + web_url: '', + }, + }, +}; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 7eec2d39a9c..45aa6050aed 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -118,6 +118,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': + case 'projects:merge_requests:new_diffs': case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); @@ -382,4 +383,4 @@ const ShortcutsBlob = require('./shortcuts_blob'); return Dispatcher; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index a510eebae1a..64a7a9eaf37 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -216,4 +216,4 @@ require('./preview_markdown'); return DropzoneInput; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 0cbf952ea5c..4b700a39d44 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -1,13 +1,14 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ -const Vue = require('vue'); -Vue.use(require('vue-resource')); +const Vue = window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); const EnvironmentsService = require('../services/environments_service'); const EnvironmentTable = require('./environments_table'); const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); module.exports = Vue.component('environment-component', { diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 867eba1d384..7bbba91bc10 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,5 +1,4 @@ const EnvironmentsComponent = require('./components/environment'); -require('../vue_shared/vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 index 29f704c1a37..d2ca465351a 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 @@ -1,5 +1,4 @@ const EnvironmentsFolderComponent = require('./environments_folder_view'); -require('../../vue_shared/vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 index 0b1204559da..53d52965758 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 @@ -1,13 +1,14 @@ /* eslint-disable no-param-reassign, no-new */ /* global Flash */ -const Vue = require('vue'); -Vue.use(require('vue-resource')); +const Vue = window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); const EnvironmentsService = require('../services/environments_service'); const EnvironmentTable = require('../components/environments_table'); const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); require('../../lib/utils/common_utils'); +require('../../vue_shared/vue_resource_interceptor'); module.exports = Vue.component('environment-folder-view', { diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index d3b58b2707a..1a489b859e8 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -13,4 +13,4 @@ return $(this).removeAttr('disabled').removeClass('disabled'); } }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 895a872568d..698870d0ce1 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -144,4 +144,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 249fe23d4cb..730104b89f9 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -39,4 +39,4 @@ return Flash; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 7f1f2a5d278..60d6658dc16 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_\'\.\+\-]|[^\\x00-\\x7a])*)$", '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[1] || match[1] === "") ? match[1] : match[2]; + return match[1]; } else { return null; } @@ -103,6 +103,9 @@ this.input.each((i, input) => { const $input = $(input); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + // This triggers at.js again + // Needed for slash commands with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); }); }, setupAtWho: function($input) { @@ -377,4 +380,4 @@ (dataToInspect === loadingState || dataToInspect.name === loadingState); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 0d618caf350..bf3da8528f0 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -846,4 +846,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js index 2e6da5750de..75a53aae33c 100644 --- a/app/assets/javascripts/graphs/stat_graph.js +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -15,4 +15,4 @@ return StatGraph; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index d06a1a5dae4..bbfb467ad50 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -113,4 +113,4 @@ window.d3 = require('d3'); return ContributorsStatGraph; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 241249fae63..228771da4ee 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -273,4 +273,4 @@ window.d3 = require('d3'); return ContributorsAuthorGraph; })(ContributorsGraph); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 29c3163328f..7954c583598 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -135,4 +135,4 @@ } } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 10dfd05fe3c..c5cb273c5b2 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -17,4 +17,4 @@ return GroupAvatar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index bc88dc2d092..6b937e7fa0f 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -68,4 +68,4 @@ return GroupsSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 9390136d3d8..34e4a257ff9 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -78,4 +78,4 @@ new window.ImporterStatus(jobsImportPath, importPath); } }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index c77fbb6a1c7..115312d4b83 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -76,4 +76,4 @@ return IssuableContext; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index c7c744ef61f..de184ab2675 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -156,4 +156,4 @@ return IssuableForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 1776b3d61f6..52457f70d90 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -3,7 +3,7 @@ require('./flash'); require('vendor/jquery.waitforimages'); -require('vendor/task_list'); +require('./task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -11,10 +11,16 @@ require('vendor/task_list'); this.Issue = (function() { function Issue() { this.submitNoteForm = bind(this.submitNoteForm, this); - // Prevent duplicate event bindings - this.disableTaskList(); if ($('a.btn-close').length) { - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); this.initIssueBtnEventListeners(); } this.initMergeRequests(); @@ -22,11 +28,6 @@ require('vendor/task_list'); this.initCanCreateBranch(); } - Issue.prototype.initTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('enable'); - return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); - }; - Issue.prototype.initIssueBtnEventListeners = function() { var _this, issueFailMessage; _this = this; @@ -85,30 +86,6 @@ require('vendor/task_list'); } }; - Issue.prototype.disableTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); - }; - - Issue.prototype.updateTaskList = function() { - var patchData; - patchData = {}; - patchData['issue'] = { - 'description': $('.js-task-list-field', this).val() - }; - return $.ajax({ - type: 'PATCH', - url: $('form.js-issuable-update').attr('action'), - data: patchData, - success: function(issue) { - document.querySelector('#task_status').innerText = issue.task_status; - document.querySelector('#task_status_short').innerText = issue.task_status_short; - } - }); - // TODO (rspeicher): Make the issue description inline-editable like a note so - // that we can re-use its form here - }; - Issue.prototype.initMergeRequests = function() { var $container; $container = $('#merge-requests'); @@ -155,4 +132,4 @@ require('vendor/task_list'); return Issue; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 1d6eff11403..b2cfd3ef2a3 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -31,4 +31,4 @@ return IssueStatusSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 40ad6fc348e..17a3fc1b1e4 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -43,4 +43,4 @@ return Labels; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index e4cf9057e6d..9e2d14c7f87 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -504,4 +504,4 @@ return LabelsSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 1c0ea317c1a..08ca9e4fa4d 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -44,4 +44,4 @@ } }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js index 5221f85ba7a..7862c6797c3 100644 --- a/app/assets/javascripts/lib/cropper.js +++ b/app/assets/javascripts/lib/cropper.js @@ -4,4 +4,4 @@ (function() { -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js index 5a9a501efe3..ebe1e2ae98d 100644 --- a/app/assets/javascripts/lib/raphael.js +++ b/app/assets/javascripts/lib/raphael.js @@ -6,4 +6,4 @@ (function() { -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js index ce090a2e4fd..d93c1d0da59 100644 --- a/app/assets/javascripts/lib/utils/animate.js +++ b/app/assets/javascripts/lib/utils/animate.js @@ -46,4 +46,4 @@ return dfd.promise(); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 764aff51fee..45a1d90a9d9 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -297,4 +297,4 @@ */ w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 index f41fa15b147..82dcbdc26c8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 +++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6 @@ -123,4 +123,4 @@ window.dateFormat = require('vendor/date.format'); return Math.floor((date2 - date1) / millisecondsPerDay); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 6d5979603b9..66f39122a66 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -44,4 +44,4 @@ w.notify = notifyMe; return w.notifyPermissions = notifyPermissions; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d9370db0cf2..f755d212b3c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ +require('vendor/latinise'); + (function() { (function(w) { var base; @@ -164,8 +166,14 @@ gl.text.pluralize = function(str, count) { return str + (count > 1 || count === 0 ? 's' : ''); }; - return gl.text.truncate = function(string, maxLength) { + gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; + gl.text.dasherize = function(str) { + return str.replace(/[_\s]+/g, '-'); + }; + gl.text.slugify = function(str) { + return str.trim().toLowerCase().latinise(); + }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index 6d813d61601..db62e0be324 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -12,4 +12,4 @@ return (obj != null) && (obj.constructor === Object); }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6 index a1558b371f0..1bc81d2e4a4 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js.es6 +++ b/app/assets/javascripts/lib/utils/url_utility.js.es6 @@ -83,4 +83,4 @@ document.location.href = url; }; })(window); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index d7137ec63e4..966fcd8ec47 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -179,4 +179,4 @@ require('vendor/jquery.scrollTo'); return LineHighlighter; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 1b0d0768db8..729baa2e1a7 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -4,4 +4,4 @@ window.addEventListener('beforeunload', function() { $('.tanuki-logo').addClass('animate'); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6 index efe7c78a8ec..129d2dc5f0a 100644 --- a/app/assets/javascripts/member_expiration_date.js.es6 +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -49,4 +49,4 @@ inputs.each(toggleClearInput); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index e65378cd610..5e01aacf2ba 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,7 +2,7 @@ /* global MergeRequestTabs */ require('vendor/jquery.waitforimages'); -require('vendor/task_list'); +require('./task_list'); require('./merge_request_tabs'); (function() { @@ -24,12 +24,18 @@ require('./merge_request_tabs'); }; })(this)); this.initTabs(); - // Prevent duplicate event bindings - this.disableTaskList(); this.initMRBtnListeners(); this.initCommitMessageListeners(); if ($("a.btn-close").length) { - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'merge_request', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); } } @@ -50,11 +56,6 @@ require('./merge_request_tabs'); return this.$('.all-commits').removeClass('hide'); }; - MergeRequest.prototype.initTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('enable'); - return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList); - }; - MergeRequest.prototype.initMRBtnListeners = function() { var _this; _this = this; @@ -85,30 +86,6 @@ require('./merge_request_tabs'); } }; - MergeRequest.prototype.disableTaskList = function() { - $('.detail-page-description .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container'); - }; - - MergeRequest.prototype.updateTaskList = function() { - var patchData; - patchData = {}; - patchData['merge_request'] = { - 'description': $('.js-task-list-field', this).val() - }; - return $.ajax({ - type: 'PATCH', - url: $('form.js-issuable-update').attr('action'), - data: patchData, - success: function(mergeRequest) { - document.querySelector('#task_status').innerText = mergeRequest.task_status; - document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short; - } - }); - // TODO (rspeicher): Make the merge request description inline-editable like a - // note so that we can re-use its form here - }; - MergeRequest.prototype.initCommitMessageListeners = function() { $(document).on('click', 'a.js-with-description-link', function(e) { var textarea = $('textarea.js-commit-message'); @@ -131,4 +108,4 @@ require('./merge_request_tabs'); return MergeRequest; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 4ab33420e59..88f08bbaa34 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -252,7 +252,6 @@ require('./smart_interval'); $('.ci_widget.ci-error').show(); this.setMergeButtonClass('btn-danger'); } - this.initMiniPipelineGraph(); }; MergeRequestWidget.prototype.showCICoverage = function(coverage) { diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 527cdc9b698..9548a98f499 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -42,4 +42,4 @@ return MergedButtons; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 051cb9fe5c5..7fbaeec7882 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -172,4 +172,4 @@ return Milestone; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2f08aa7fe8b..8df1c8e7f94 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -199,4 +199,4 @@ return MilestoneSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 919fcd0a07b..2145e531331 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -28,7 +28,7 @@ * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - $(document).on('shown.bs.dropdown', this.container, this.getBuildsList); + $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); } /** diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 2ae5617206e..b98e6121967 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -83,4 +83,4 @@ return NamespaceSelects; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index a7ccd03b60c..43dc9838977 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -421,4 +421,4 @@ y: h }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 37bf6436fd1..8e7027b44e7 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -17,4 +17,4 @@ return Network; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index b4491354472..aae509caa79 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -19,4 +19,4 @@ requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); }); return new ShortcutsNetwork(network_graph.branch_graph); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 7f763c13b50..cb24f212c66 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -100,4 +100,4 @@ return NewBranchForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 41eea78a3e6..747f693726e 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -30,4 +30,4 @@ return NewCommitForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 0464b895d6d..03504255bda 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -11,7 +11,7 @@ require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.atwho'); -require('vendor/task_list'); +require('./task_list'); (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -51,7 +51,11 @@ require('vendor/task_list'); this.addBinding(); this.setPollingInterval(); this.setupMainTargetNoteForm(); - this.initTaskList(); + this.taskList = new gl.TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes' + }); this.collapseLongCommitList(); // We are in the Merge Requests page so we need another edit form for Changes tab @@ -125,8 +129,6 @@ require('vendor/task_list'); $(document).off("keydown", ".js-note-text"); $(document).off('click', '.js-comment-resolve-button'); $(document).off("click", '.system-note-commit-list-toggler'); - $('.note .js-task-list-container').taskList('disable'); - return $(document).off('tasklist:changed', '.note .js-task-list-container'); }; Notes.prototype.keydownNoteText = function(e) { @@ -286,7 +288,7 @@ require('vendor/task_list'); // Update datetime format on the recent note gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); this.collapseLongCommitList(); - this.initTaskList(); + this.taskList.init(); this.refresh(); return this.updateNotesCount(1); } @@ -863,15 +865,6 @@ require('vendor/task_list'); } }; - Notes.prototype.initTaskList = function() { - this.enableTaskList(); - return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this)); - }; - - Notes.prototype.enableTaskList = function() { - return $('.note .js-task-list-container').taskList('enable'); - }; - Notes.prototype.putEditFormInPlace = function($el) { var $editForm = $(this.getEditFormSelector($el)); var $note = $el.closest('.note'); @@ -896,17 +889,6 @@ require('vendor/task_list'); $editForm.find('.referenced-users').hide(); }; - Notes.prototype.updateTaskList = function(e) { - var $target = $(e.target); - var $list = $target.closest('.js-task-list-container'); - var $editForm = $(this.getEditFormSelector($target)); - var $note = $list.closest('.note'); - - this.putEditFormInPlace($list); - $editForm.find('#note_note').val($note.find('.original-task-list').val()); - $('form', $list).submit(); - }; - Notes.prototype.updateNotesCount = function(updateCount) { return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); }; @@ -955,4 +937,4 @@ require('vendor/task_list'); return Notes; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 926dc35fee8..838356133cd 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -28,4 +28,4 @@ return NotificationsDropdown; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index c3d7cc0adfb..5005af90d48 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -54,4 +54,4 @@ return NotificationsForm; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 71719917d0c..7c03c8b72d4 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -126,4 +126,4 @@ return Project; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index a6d3ba9eb86..aabdfbf65e2 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -17,4 +17,4 @@ return ProjectAvatar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 04fe84683f3..e01668eabef 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -168,4 +168,4 @@ return ProjectFindFile; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 208f25a0e33..47197db39d3 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -10,4 +10,4 @@ return ProjectFork; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index d7943959238..08334bf1ec5 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -10,4 +10,4 @@ return ProjectImport; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index 8365f7118d5..0a811627600 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -38,13 +38,15 @@ this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - for (const button of this.$buttons) { + this.$buttons.map((button) => { const $button = $(button); if ($button.attr('data-original-title')) { $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); } - } + + return button; + }); }); } } diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 3aa6f6771ce..e9927c1bf51 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -101,4 +101,4 @@ return ProjectNew; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 7b5e9953598..f80e765ce30 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -101,4 +101,4 @@ return ProjectSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index aad130cf267..3a51c1f26ac 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -6,6 +6,6 @@ return ProjectShow; })(); -}).call(this); +}).call(window); // I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 69a11dfaf39..acdf9b7eb5a 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -47,4 +47,4 @@ }); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index bdbad93ad04..48cae8a4fa9 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -12,4 +12,4 @@ $(document).on('ready load', function() { return $('body').renderGFM(); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 6cef449babf..76c61c001ba 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -51,4 +51,4 @@ }); } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 76a0f993ea0..903862cac6b 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -21,11 +21,16 @@ }; Sidebar.prototype.addEventListeners = function() { + const $document = $(document); + const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); + this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(document).on('click', '.js-sidebar-toggle', function(e, triggered) { + $(window).on('resize', () => throttledSetSidebarHeight()); + $document.on('scroll', () => throttledSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); $this = $(this); @@ -191,6 +196,17 @@ } }; + Sidebar.prototype.setSidebarHeight = function() { + const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const $rightSidebar = $('.js-right-sidebar'); + const diff = $navHeight - $('body').scrollTop(); + if (diff > 0) { + $rightSidebar.outerHeight($(window).height() - diff); + } else { + $rightSidebar.outerHeight('100%'); + } + }; + Sidebar.prototype.isOpen = function() { return this.sidebar.is('.right-sidebar-expanded'); }; @@ -201,4 +217,4 @@ return Sidebar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index b1c0dc37b4d..e66418beeab 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -97,4 +97,4 @@ return Search; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c6d9b007ad1..81766f4bd55 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -97,4 +97,4 @@ } }; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 7378b322426..e7baea894f6 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -37,4 +37,4 @@ require('./shortcuts'); return ShortcutsDashboardNavigation; })(Shortcuts); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 36e379d634d..a27ac264a5c 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -35,4 +35,4 @@ require('./shortcuts_navigation'); return ShortcutsFindFile; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b841abb754d..fe58e98cee5 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -89,4 +89,4 @@ require('./shortcuts_navigation'); return ShortcutsIssuable; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index cb5f2c53ea6..542cd586df0 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -65,4 +65,4 @@ require('./shortcuts'); return ShortcutsNavigation; })(Shortcuts); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 651957f5325..4c2bf8bf001 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -25,4 +25,4 @@ require('./shortcuts_navigation'); return ShortcutsNetwork; })(ShortcutsNavigation); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 deleted file mode 100644 index 33e4b7db681..00000000000 --- a/app/assets/javascripts/sidebar.js.es6 +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */ -/* global Cookies */ - -(() => { - const pinnedStateCookie = 'pin_nav'; - const sidebarBreakpoint = 1024; - - const pageSelector = '.page-with-sidebar'; - const navbarSelector = '.navbar-gitlab'; - const sidebarWrapperSelector = '.sidebar-wrapper'; - const sidebarContentSelector = '.nav-sidebar'; - - const pinnedToggleSelector = '.js-nav-pin'; - const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle'; - - const pinnedPageClass = 'page-sidebar-pinned'; - const expandedPageClass = 'page-sidebar-expanded'; - - const pinnedNavbarClass = 'header-sidebar-pinned'; - const expandedNavbarClass = 'header-sidebar-expanded'; - - class Sidebar { - constructor() { - if (!Sidebar.singleton) { - Sidebar.singleton = this; - Sidebar.singleton.init(); - } - - return Sidebar.singleton; - } - - init() { - this.isPinned = Cookies.get(pinnedStateCookie) === 'true'; - this.isExpanded = ( - window.innerWidth >= sidebarBreakpoint && - $(pageSelector).hasClass(expandedPageClass) - ); - $(window).on('resize', () => this.setSidebarHeight()); - $(document) - .on('click', sidebarToggleSelector, () => this.toggleSidebar()) - .on('click', pinnedToggleSelector, () => this.togglePinnedState()) - .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e)) - .on('DOMContentLoaded', () => this.renderState()) - .on('scroll', () => this.setSidebarHeight()) - .on('todo:toggle', (e, count) => this.updateTodoCount(count)); - this.renderState(); - this.setSidebarHeight(); - } - - handleClickEvent(e) { - if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) { - const $target = $(e.target); - const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0; - const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0; - if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) { - this.toggleSidebar(); - } - } - } - - updateTodoCount(count) { - $('.js-todos-count').text(gl.text.addDelimiter(count)); - } - - toggleSidebar() { - this.isExpanded = !this.isExpanded; - this.renderState(); - } - - setSidebarHeight() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); - const diff = $navHeight - $('body').scrollTop(); - if (diff > 0) { - $('.js-right-sidebar').outerHeight($(window).height() - diff); - } else { - $('.js-right-sidebar').outerHeight('100%'); - } - } - - togglePinnedState() { - this.isPinned = !this.isPinned; - if (!this.isPinned) { - this.isExpanded = false; - } - Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 }); - this.renderState(); - } - - renderState() { - $(pageSelector) - .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded) - .toggleClass(expandedPageClass, this.isExpanded); - $(navbarSelector) - .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded) - .toggleClass(expandedNavbarClass, this.isExpanded); - - const $pinnedToggle = $(pinnedToggleSelector); - const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation'; - const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide'; - $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState); - - if (this.isExpanded) { - const sidebarContent = $(sidebarContentSelector); - setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200); - } - } - } - - window.gl = window.gl || {}; - gl.Sidebar = Sidebar; -})(); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 3ee0c73a8d2..294d087554e 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -95,4 +95,4 @@ } }); }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 64f9065be42..89822246bb8 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -13,4 +13,4 @@ requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); $(".snippet-file-content").val(editor.getValue()); }); }); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 531fd0e9c32..c75b44cc2fd 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -27,4 +27,4 @@ return Star; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 187356f0bf9..8b25f43ffc7 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -31,4 +31,4 @@ return SubscriptionSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 115716bff6a..7c063fae045 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -24,4 +24,4 @@ } } }; -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js new file mode 100644 index 00000000000..dfe24d1fb33 --- /dev/null +++ b/app/assets/javascripts/task_list.js @@ -0,0 +1,40 @@ +require('vendor/task_list'); + +class TaskList { + constructor(options = {}) { + this.selector = options.selector; + this.dataType = options.dataType; + this.fieldName = options.fieldName; + this.onSuccess = options.onSuccess || (() => {}); + this.init(); + } + + init() { + // Prevent duplicate event bindings + this.disable(); + $(`${this.selector} .js-task-list-container`).taskList('enable'); + $(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this)); + } + + disable() { + $(`${this.selector} .js-task-list-container`).taskList('disable'); + $(document).off('tasklist:changed', `${this.selector} .js-task-list-container`); + } + + update(e) { + const $target = $(e.target); + const patchData = {}; + patchData[this.dataType] = { + [this.fieldName]: $target.val(), + }; + return $.ajax({ + type: 'PATCH', + url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), + data: patchData, + success: this.onSuccess, + }); + } +} + +window.gl = window.gl || {}; +window.gl.TaskList = TaskList; diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index ded683f2ca1..e9513725d9d 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,28 +1,34 @@ -/* 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 */ +/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */ /* global UsersSelect */ ((global) => { class Todos { - constructor({ el } = {}) { - this.allDoneClicked = this.allDoneClicked.bind(this); - this.doneClicked = this.doneClicked.bind(this); - this.el = el || $('.js-todos-options'); - this.perPage = this.el.data('perPage'); - this.clearListeners(); - this.initBtnListeners(); + constructor() { this.initFilters(); + this.bindEvents(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); } - clearListeners() { - $('.done-todo').off('click'); - $('.js-todos-mark-all').off('click'); - return $('.todo').off('click'); + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); } - initBtnListeners() { - $('.done-todo').on('click', this.doneClicked); - $('.js-todos-mark-all').on('click', this.allDoneClicked); - return $('.todo').on('click', this.goToTodoUrl); + unbindEvents() { + $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper); + $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); + $('.todo').off('click', this.goToTodoUrl); + } + + bindEvents() { + this.updateStateClickedWrapper = this.updateStateClicked.bind(this); + this.allDoneClickedWrapper = this.allDoneClicked.bind(this); + + $('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper); + $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); + $('.todo').on('click', this.goToTodoUrl); } initFilters() { @@ -33,7 +39,7 @@ $('form.filter-form').on('submit', function (event) { event.preventDefault(); - gl.utils.visitUrl(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); }); } @@ -44,105 +50,72 @@ filterable: searchFields ? true : false, search: { fields: searchFields }, data: $dropdown.data('data'), - clicked: function() { + clicked: function () { return $dropdown.closest('form.filter-form').submit(); - } + }, }); } - doneClicked(e) { + updateStateClicked(e) { e.preventDefault(); - e.stopImmediatePropagation(); - const $target = $(e.currentTarget); - $target.disable(); - return $.ajax({ + const target = e.target; + target.setAttribute('disabled', ''); + target.classList.add('disabled'); + $.ajax({ type: 'POST', - url: $target.attr('href'), + url: target.getAttribute('href'), dataType: 'json', data: { - '_method': 'delete' + '_method': target.getAttribute('data-method'), }, success: (data) => { - this.redirectIfNeeded(data.count); - this.clearDone($target.closest('li')); - return this.updateBadges(data); - } + this.updateState(target); + this.updateBadges(data); + }, }); } allDoneClicked(e) { e.preventDefault(); - e.stopImmediatePropagation(); const $target = $(e.currentTarget); $target.disable(); - return $.ajax({ + $.ajax({ type: 'POST', url: $target.attr('href'), dataType: 'json', data: { - '_method': 'delete' + '_method': 'delete', }, success: (data) => { $target.remove(); $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>'); - return this.updateBadges(data); - } + this.updateBadges(data); + }, }); } - clearDone($row) { - const $ul = $row.closest('ul'); - $row.remove(); - if (!$ul.find('li').length) { - return $ul.parents('.panel').remove(); + updateState(target) { + const row = target.closest('li'); + const restoreBtn = row.querySelector('.js-undo-todo'); + const doneBtn = row.querySelector('.js-done-todo'); + + target.removeAttribute('disabled'); + target.classList.remove('disabled'); + target.classList.add('hidden'); + + if (target === doneBtn) { + row.classList.add('done-reversible'); + restoreBtn.classList.remove('hidden'); + } else { + row.classList.remove('done-reversible'); + doneBtn.classList.remove('hidden'); } } updateBadges(data) { $(document).trigger('todo:toggle', data.count); $('.todos-pending .badge').text(data.count); - return $('.todos-done .badge').text(data.done_count); - } - - getTotalPages() { - return this.el.data('totalPages'); - } - - getCurrentPage() { - return this.el.data('currentPage'); - } - - getTodosPerPage() { - return this.el.data('perPage'); - } - - redirectIfNeeded(total) { - const currPages = this.getTotalPages(); - const currPage = this.getCurrentPage(); - - // Refresh if no remaining Todos - if (!total) { - window.location.reload(); - return; - } - // Do nothing if no pagination - if (!currPages) { - return; - } - - const newPages = Math.ceil(total / this.getTodosPerPage()); - let url = location.href; - - if (newPages !== currPages) { - // Redirect to previous page if there's one available - if (currPages > 1 && currPage === currPages) { - const pageParams = { - page: currPages - 1 - }; - url = gl.utils.mergeUrlParams(pageParams, url); - } - return gl.utils.visitUrl(url); - } + $('.todos-done .badge').text(data.done_count); } goToTodoUrl(e) { @@ -159,12 +132,12 @@ if (selected.tagName === 'IMG') { const avatarUrl = selected.parentElement.getAttribute('href'); - return window.open(avatarUrl, windowTarget); + window.open(avatarUrl, windowTarget); } else { - return window.open(todoLink, windowTarget); + window.open(todoLink, windowTarget); } } else { - return gl.utils.visitUrl(todoLink); + gl.utils.visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index b1b35fdbd6c..76a821c7a17 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -65,4 +65,4 @@ return TreeView; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 86b459e1866..fd1829efe18 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -24,4 +24,4 @@ return U2FError; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 69d1ff3a39e..17631f2908d 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -95,4 +95,4 @@ return U2FRegister; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 34e88220b12..813d363db00 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -9,4 +9,4 @@ return U2FUtil; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 6e40dfdf3d8..5111b260e1c 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -221,4 +221,4 @@ return Calendar; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index d4b24d13299..de33a31b411 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -432,4 +432,4 @@ return UsersSelect; })(); -}).call(this); +}).call(window); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index dac364977d5..0265c00a414 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -29,7 +29,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s }, props: ['scope', 'store', 'svgs'], created() { - const pagenum = gl.utils.getParameterByName('p'); + const pagenum = gl.utils.getParameterByName('page'); const scope = gl.utils.getParameterByName('scope'); if (pagenum) this.pagenum = pagenum; if (scope) this.apiScope = scope; @@ -44,7 +44,6 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s }, methods: { - /** * Changes the URL according to the pagination component. * @@ -57,7 +56,7 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s */ change(pagenum, apiScope) { if (!apiScope) apiScope = 'all'; - gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`); }, }, template: ` diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ef99b2e92f0..75fd1394a03 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,14 +1,10 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -require('vendor/latinise'); require('./breakpoints'); require('vendor/jquery.nicescroll'); ((global) => { - const dasherize = str => str.replace(/[_\s]+/g, '-'); - const slugify = str => dasherize(str.trim().toLowerCase().latinise()); - class Wikis { constructor() { this.bp = Breakpoints.get(); @@ -34,7 +30,7 @@ require('vendor/jquery.nicescroll'); if (!this.newWikiForm) return; const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = slugify(slugInput.value); + const slug = gl.text.slugify(slugInput.value); if (slug.length > 0) { const wikisPath = slugInput.getAttribute('data-wikis-path'); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index d9261cda1b1..ce626cf7b46 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -94,4 +94,4 @@ require('mousetrap/plugins/pause/mousetrap-pause'); return ZenMode; })(); -}).call(this); +}).call(window); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 08f203a1bf6..39cf3b5f8ae 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,7 +19,6 @@ @import "framework/flash.scss"; @import "framework/forms.scss"; @import "framework/gfm.scss"; -@import "framework/gitlab-theme.scss"; @import "framework/header.scss"; @import "framework/highlight.scss"; @import "framework/issue_box.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 0ca5a9343f7..90935b9616b 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -116,7 +116,7 @@ } .btn, -.side-nav-toggle { +.global-dropdown-toggle { @include transition(background-color, border-color, color, box-shadow); } @@ -140,7 +140,6 @@ a { @include transition(background-color, box-shadow); } -.nav-sidebar a, .dropdown-menu a, .dropdown-menu button, .dropdown-menu-nav a { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss deleted file mode 100644 index d6566dc4ec9..00000000000 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Styles the GitLab application with a specific color theme - * - * $color-light - - * $color - - * $color-darker - - * $color-dark - - */ -@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { - .page-with-sidebar { - .toggle-nav-collapse, - .pin-nav-btn { - color: $color-light; - - &:hover { - color: $white-light; - } - } - - .sidebar-wrapper { - background: $color-darker; - } - - .sidebar-action-buttons { - color: $color-light; - background-color: lighten($color-darker, 5%); - } - - .nav-sidebar { - li { - a { - color: $color-light; - - &:hover, - &:focus, - &:active { - background: $color-dark; - } - - i { - color: $color-light; - } - - path, - polygon { - fill: $color-light; - } - - .count { - color: $color-light; - background: $color-dark; - } - - svg { - position: relative; - top: 3px; - } - } - - &.separate-item { - border-top: 1px solid $color; - } - - &.active a { - color: $white-light; - background: $color-dark; - - &.no-highlight { - border: none; - } - - i { - color: $white-light; - } - - path, - polygon { - fill: $white-light; - } - } - } - - .about-gitlab { - color: $color-light; - } - } - } -} - -$theme-charcoal-light: #b9bbbe; -$theme-charcoal: #485157; -$theme-charcoal-dark: #3d454d; -$theme-charcoal-darker: #383f45; - -$theme-blue-light: #becde9; -$theme-blue: #2980b9; -$theme-blue-dark: #1970a9; -$theme-blue-darker: #096099; - -$theme-graphite-light: #ccc; -$theme-graphite: #777; -$theme-graphite-dark: #666; -$theme-graphite-darker: #555; - -$theme-black-light: #979797; -$theme-black: #373737; -$theme-black-dark: #272727; -$theme-black-darker: #222; - -$theme-green-light: #adc; -$theme-green: #019875; -$theme-green-dark: #018865; -$theme-green-darker: #017855; - -$theme-violet-light: #98c; -$theme-violet: #548; -$theme-violet-dark: #436; -$theme-violet-darker: #325; - -body { - &.ui_blue { - @include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker); - } - - &.ui_charcoal { - @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker); - } - - &.ui_graphite { - @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker); - } - - &.ui_black { - @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker); - } - - &.ui_green { - @include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker); - } - - &.ui_violet { - @include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker); - } -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 34e010e0e8a..78434b99b62 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -100,23 +100,40 @@ header { } } } + } - .side-nav-toggle { - position: absolute; - left: -10px; - margin: 7px 0; - font-size: 18px; - padding: 6px 10px; - border: none; - background-color: $gray-light; + .global-dropdown { + position: absolute; + left: -10px; - &:hover { - background-color: $white-normal; - color: $gl-header-nav-hover-color; + .badge { + font-size: 11px; + } + + li { + &.active a { + font-weight: bold; } } } + .global-dropdown-toggle { + margin: 7px 0; + font-size: 18px; + padding: 6px 10px; + border: none; + background-color: $gray-light; + + &:hover { + background-color: $white-normal; + } + + &:focus { + outline: none; + background-color: $white-normal; + } + } + .header-content { position: relative; height: $header-height; @@ -131,16 +148,11 @@ header { } .header-logo { - position: absolute; - left: 50%; + display: inline-block; + margin: 0 8px 0 3px; + position: relative; top: 7px; transition-duration: .3s; - z-index: 999; - - #logo { - position: relative; - left: -50%; - } svg, img { @@ -150,15 +162,6 @@ header { &:hover { cursor: pointer; } - - @media (max-width: $screen-xs-max) { - right: 20px; - left: auto; - - #logo { - left: auto; - } - } } .title { @@ -166,7 +169,7 @@ header { padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; + max-width: 450px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -176,10 +179,6 @@ header { vertical-align: top; white-space: nowrap; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - @media (max-width: $screen-xs-max) { max-width: 190px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 674d3bb45aa..7d4a814a36c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,7 +1,7 @@ @mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; - z-index: 2; + z-index: 1; position: absolute; bottom: 12px; width: 43px; @@ -18,7 +18,7 @@ .fa { position: relative; - top: 5px; + top: 6px; font-size: 18px; } } @@ -79,7 +79,6 @@ } &.sub-nav { - text-align: center; background-color: $gray-normal; .container-fluid { @@ -287,7 +286,6 @@ background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; - text-align: center; .container-fluid { position: relative; @@ -353,7 +351,7 @@ right: -5px; .fa { - right: -7px; + right: -28px; } } @@ -383,7 +381,7 @@ left: 0; .fa { - left: 10px; + left: -4px; } } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 20bcb1eeb23..d09b1c9d7f5 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,36 +1,3 @@ -.page-with-sidebar { - padding-bottom: 25px; - transition: padding $sidebar-transition-duration; - - &.page-sidebar-pinned { - .sidebar-wrapper { - box-shadow: none; - } - } - - .sidebar-wrapper { - position: fixed; - top: 0; - bottom: 0; - left: 0; - height: 100%; - width: 0; - overflow: hidden; - transition: width $sidebar-transition-duration; - box-shadow: 2px 0 16px 0 $black-transparent; - } -} - -.sidebar-wrapper { - z-index: 1000; - background: $gray-light; - - .nicescroll-rails-hr { - // TODO: Figure out why nicescroll doesn't hide horizontal bar - display: none!important; - } -} - .content-wrapper { width: 100%; transition: padding $sidebar-transition-duration; @@ -47,105 +14,6 @@ } } -.nav-sidebar { - position: absolute; - top: 50px; - bottom: 0; - width: $sidebar_width; - overflow-y: auto; - overflow-x: hidden; - - &.navbar-collapse { - padding: 0 !important; - } - - li { - &.separate-item { - padding-top: 10px; - margin-top: 10px; - } - - .icon-container { - width: 34px; - display: inline-block; - text-align: center; - } - - a { - padding: 7px $gl-sidebar-padding; - font-size: $gl-font-size; - line-height: 24px; - display: block; - text-decoration: none; - font-weight: normal; - - &:hover, - &:active, - &:focus { - text-decoration: none; - } - - i { - font-size: 16px; - } - - i, - svg { - margin-right: 13px; - } - } - } - - .count { - float: right; - padding: 0 8px; - border-radius: 6px; - } - - .about-gitlab { - padding: 7px $gl-sidebar-padding; - font-size: $gl-font-size; - line-height: 24px; - display: block; - text-decoration: none; - font-weight: normal; - position: absolute; - bottom: 10px; - } -} - -.sidebar-action-buttons { - width: $sidebar_width; - position: absolute; - top: 0; - left: 0; - min-height: 50px; - padding: 5px 0; - font-size: 18px; - line-height: 30px; - - .toggle-nav-collapse { - left: 0; - } - - .pin-nav-btn { - right: 0; - display: none; - - @media (min-width: $sidebar-breakpoint) { - display: block; - } - - .fa { - transition: transform .15s; - - .page-sidebar-pinned & { - transform: rotate(90deg); - } - } - } -} - .nav-header-btn { padding: 10px $gl-sidebar-padding; color: inherit; @@ -161,59 +29,16 @@ } } -.page-sidebar-expanded { - .sidebar-wrapper { - width: $sidebar_width; - } -} - -.page-sidebar-pinned { - .content-wrapper, - .layout-nav { - @media (min-width: $sidebar-breakpoint) { - padding-left: $sidebar_width; - } - } - - .merge-request-tabs-holder.affix { - @media (min-width: $sidebar-breakpoint) { - left: $sidebar_width; - } - } - - &.right-sidebar-expanded { - .line-resolve-all-container { - @media (min-width: $sidebar-breakpoint) { - display: none; - } - } - } -} - -header.header-sidebar-pinned { - @media (min-width: $sidebar-breakpoint) { - padding-left: ($sidebar_width + $gl-padding); - - .side-nav-toggle { - display: none; - } - - .header-content { - padding-left: 0; - } - } -} - .right-sidebar-collapsed { padding-right: 0; @media (min-width: $screen-sm-min) { .content-wrapper { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } .merge-request-tabs-holder.affix { - right: $sidebar_collapsed_width; + right: $gutter_collapsed_width; } } @@ -231,7 +56,7 @@ header.header-sidebar-pinned { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { &:not(.build-sidebar):not(.wiki-sidebar) { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } } @@ -245,12 +70,12 @@ header.header-sidebar-pinned { } &.with-overlay .merge-request-tabs-holder.affix { - right: $sidebar_collapsed_width; + right: $gutter_collapsed_width; } } &.with-overlay { - padding-right: $sidebar_collapsed_width; + padding-right: $gutter_collapsed_width; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7809d4866f1..ba0af072716 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,8 +1,6 @@ /* * Layout */ -$sidebar_collapsed_width: 62px; -$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 250px; @@ -541,4 +539,4 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; -$action-icon-color: #d6d6d6;
\ No newline at end of file +$action-icon-color: #d6d6d6; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a53cc27fac9..4426169ef5a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -253,11 +253,11 @@ display: block; } - width: $sidebar_collapsed_width; + width: $gutter_collapsed_width; padding-top: 0; .block { - width: $sidebar_collapsed_width - 2px; + width: $gutter_collapsed_width - 2px; margin-left: -19px; padding: 15px 0 0; border-bottom: none; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 3da1150f89b..27c47d36818 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -30,6 +30,26 @@ word-wrap: break-word; } } + + .panel-heading { + line-height: $line-height-base; + padding: 14px 16px; + display: -webkit-flex; + display: flex; + + .title { + -webkit-flex: 1; + -webkit-flex-grow: 1; + flex: 1; + flex-grow: 2; + } + + .counter { + -webkit-flex: 1; + flex: 0; + padding-left: 16px; + } + } } .milestone-summary { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 00eb5b30fd5..3fe1eef307e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -222,6 +222,11 @@ } } + .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } + .dropdown-toggle, .dropdown-menu { color: $gl-text-color-secondary; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 100ace41f2a..305feaacaa1 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,42 +1,3 @@ -.application-theme { - label { - margin-right: 20px; - text-align: center; - - .preview { - border-radius: 4px; - - height: 80px; - margin-bottom: 10px; - width: 160px; - - &.ui_blue { - background: $theme-blue; - } - - &.ui_charcoal { - background: $theme-charcoal; - } - - &.ui_graphite { - background: $theme-graphite; - } - - &.ui_black { - background: $theme-black; - } - - &.ui_green { - background: $theme-green; - } - - &.ui_violet { - background: $theme-violet; - } - } - } -} - .syntax-theme { label { margin-right: 20px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6b05e5bb4aa..8c0de314420 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -268,6 +268,13 @@ } } +.project-repo-buttons { + .project-action-button .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } +} + .split-one { display: inline-table; margin-right: 12px; @@ -645,29 +652,23 @@ pre.light-well { } } -.project-last-commit { - @media (min-width: $screen-sm-min) { - margin-top: $gl-padding; +.container-fluid.project-stats-container { + @media (max-width: $screen-xs-max) { + padding: 12px 0; } +} - &.container-fluid { - padding-top: 12px; - padding-bottom: 12px; - background-color: $gray-light; - border: 1px solid $border-color; - border-right-width: 0; - border-left-width: 0; +.project-last-commit { + background-color: $gray-light; + padding: 12px $gl-padding; + border: 1px solid $border-color; - @media (min-width: $screen-sm-min) { - border-right-width: 1px; - border-left-width: 1px; - } + @media (min-width: $screen-sm-min) { + margin-top: $gl-padding; } - &.container-limited { - @media (min-width: 1281px) { - border-radius: $border-radius-base; - } + @media (min-width: $screen-sm-min) { + border-radius: $border-radius-base; } .ci-status { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 0d5604aae69..551a66fbf3a 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -60,6 +60,18 @@ } } +.todos-list > .todo.todo-pending.done-reversible { + background-color: $gray-light; + + &:hover { + border-color: $border-color; + } + + .title { + font-weight: normal; + } +} + .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 948921efc0b..e4487dbcb87 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -149,7 +149,7 @@ } .commit-actions { - width: 200px; + width: 260px; } } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 0ff3c3f5472..6cc1cc8e263 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -31,7 +31,6 @@ nav.navbar-collapse.collapse, .blob-commit-info, .file-title, .file-holder, -.sidebar-wrapper, .nav, .btn, ul.notes-form, diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index 338496013a0..c09095b9849 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -2,5 +2,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) + @concurrency = Sidekiq.options[:concurrency] end end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 7345c91f67d..348641e5ecb 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController end def update - if @runner.update_attributes(runner_params) + if Ci::UpdateRunnerService.new(@runner).update(runner_params) respond_to do |format| format.js format.html { redirect_to admin_runner_path(@runner) } @@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController end def resume - if @runner.update_attributes(active: true) + if Ci::UpdateRunnerService.new(@runner).update(active: true) redirect_to admin_runners_path, notice: 'Runner was successfully updated.' else redirect_to admin_runners_path, alert: 'Runner was not updated.' @@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController end def pause - if @runner.update_attributes(active: false) + if Ci::UpdateRunnerService.new(@runner).update(active: false) redirect_to admin_runners_path, notice: 'Runner was successfully updated.' else redirect_to admin_runners_path, alert: 'Runner was not updated.' diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index ca04a17caa1..1330399a836 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -21,6 +21,7 @@ class Admin::SystemInfoController < Admin::ApplicationController 'mqueue', 'proc', 'pstore', + 'rpc_pipefs', 'securityfs', 'sysfs', 'tmpfs', diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1cd50852e89..7ffde71c3b1 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, - :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 6286d67d30c..88d180fcc2e 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -104,23 +104,15 @@ module CreatesCommit if can?(current_user, :push_code, @project) # Edit file in this project @mr_source_project = @project - - if @project.forked? - # Merge request from this project to fork origin - @mr_target_project = @project.forked_from_project - @mr_target_branch = @mr_target_project.repository.root_ref - else - # Merge request to this project - @mr_target_project = @project - @mr_target_branch = @ref || @target_branch - end else # Merge request from fork to this project @mr_source_project = current_user.fork_of(@project) - @mr_target_project = @project - @mr_target_branch = @ref || @target_branch end + # Merge request to this project + @mr_target_project = @project + @mr_target_branch = @ref || @target_branch + @mr_source_branch = guess_mr_source_branch end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb new file mode 100644 index 00000000000..ca6dffe1cc5 --- /dev/null +++ b/app/controllers/concerns/snippets_actions.rb @@ -0,0 +1,21 @@ +module SnippetsActions + extend ActiveSupport::Concern + + def edit + end + + def raw + send_data( + convert_line_endings(@snippet.content), + type: 'text/plain; charset=utf-8', + disposition: 'inline', + filename: @snippet.sanitized_file_name + ) + end + + private + + def convert_line_endings(content) + params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n") + end +end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index e3933e3d7b1..4e61b0886d8 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -29,6 +29,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController end end + def restore + TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user) + + render json: todos_counts + end + private def find_todos diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f54c79c2e37..3ab7e6e0658 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController handle_omniauth end + def authentiq + if params['sid'] + handle_service_ticket oauth['provider'], params['sid'] + end + handle_omniauth + end + private def handle_omniauth diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index a9a06ecc808..0d891ef4004 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController :layout, :dashboard, :project_view, - :theme_id ) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index db33b60b229..e2f81b09adc 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - @show_changes_tab = params[:view].present? cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 1d286ca62e5..ebc07add319 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -245,6 +245,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html do define_new_vars + @show_changes_tab = true render "new" end format.json do @@ -616,6 +617,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @labels = LabelsFinder.new(current_user, project_id: @project.id).execute + @show_changes_tab = params[:show_changes].present? + define_pipelines_vars end diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 74c54037ba9..8b50ea207a5 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController end def update - if @runner.update_attributes(runner_params) + if Ci::UpdateRunnerService.new(@runner).update(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else render 'edit' @@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController end def resume - if @runner.update_attributes(active: true) + if Ci::UpdateRunnerService.new(@runner).update(active: true) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else redirect_to runner_path(@runner), alert: 'Runner was not updated.' @@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController end def pause - if @runner.update_attributes(active: false) + if Ci::UpdateRunnerService.new(@runner).update(active: false) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else redirect_to runner_path(@runner), alert: 'Runner was not updated.' diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 5d193f26a8e..ef5d3d242eb 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,7 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :module_enabled before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -49,9 +50,6 @@ class Projects::SnippetsController < Projects::ApplicationController end end - def edit - end - def update UpdateSnippetService.new(project, current_user, @snippet, snippet_params).execute @@ -74,15 +72,6 @@ class Projects::SnippetsController < Projects::ApplicationController redirect_to namespace_project_snippets_path(@project.namespace, @project) end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - protected def snippet diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index b169d993688..366804ab17e 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,6 +1,7 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -47,9 +48,6 @@ class SnippetsController < ApplicationController respond_with @snippet.becomes(Snippet) end - def edit - end - def update UpdateSnippetService.new(nil, current_user, @snippet, snippet_params).execute @@ -67,18 +65,9 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - def download send_data( - @snippet.content, + convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', filename: @snippet.sanitized_file_name ) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 1576fc80a6b..206c92fe82a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -16,6 +16,7 @@ # label_name: string # sort: string # non_archived: boolean +# iids: integer[] # class IssuableFinder NONE = '0' @@ -40,6 +41,7 @@ class IssuableFinder items = by_label(items) items = by_due_date(items) items = by_non_archived(items) + items = by_iids(items) sort(items) end @@ -266,16 +268,11 @@ class IssuableFinder end def by_search(items) - if search - items = - if search =~ iid_pattern - items.where(iid: $~[:iid]) - else - items.full_search(search) - end - end + search ? items.full_search(search) : items + end - items + def by_iids(items) + params[:iids].present? ? items.where(iid: params[:iids]) : items end def sort(items) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 707eddd4d29..f542f72a386 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -26,10 +26,6 @@ class IssuesFinder < IssuableFinder IssuesFinder.not_restricted_by_confidentiality(current_user) end - def iid_pattern - @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z} - end - def self.not_restricted_by_confidentiality(user) return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 8b82255445e..b76ca389f38 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -20,14 +20,4 @@ class MergeRequestsFinder < IssuableFinder def klass MergeRequest end - - private - - def iid_pattern - @iid_pattern ||= %r{\A[ - #{Regexp.escape(MergeRequest.reference_prefix)} - #{Regexp.escape(Issue.reference_prefix)} - ](?<iid>\d+)\z - }x - end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index e21178c7377..c1523b4dabf 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,10 +1,4 @@ module NavHelper - def page_sidebar_class - if pinned_nav? - "page-sidebar-expanded page-sidebar-pinned" - end - end - def page_gutter_class if current_path?('merge_requests#show') || current_path?('merge_requests#diffs') || @@ -32,10 +26,6 @@ module NavHelper class_name = '' class_name << " with-horizontal-nav" if defined?(nav) && nav - if pinned_nav? - class_name << " header-sidebar-expanded header-sidebar-pinned" - end - class_name end @@ -46,8 +36,4 @@ module NavHelper def nav_control_class "nav-control" if current_user end - - def pinned_nav? - cookies[:pin_nav] == 'true' - end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index dd0a4ea03f0..c3a08d76318 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -41,10 +41,6 @@ module PreferencesHelper ] end - def user_application_theme - Gitlab::Themes.for_user(current_user).css_class - end - def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8c1b076c2d7..e018f8e7c4e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -62,33 +62,10 @@ module Ci new_build.save end - def retry(build, user = nil) - new_build = Ci::Build.create( - ref: build.ref, - tag: build.tag, - options: build.options, - commands: build.commands, - tag_list: build.tag_list, - project: build.project, - pipeline: build.pipeline, - name: build.name, - allow_failure: build.allow_failure, - stage: build.stage, - stage_idx: build.stage_idx, - trigger_request: build.trigger_request, - yaml_variables: build.yaml_variables, - when: build.when, - user: user, - environment: build.environment, - status_event: 'enqueue' - ) - - MergeRequests::AddTodoWhenBuildFailsService - .new(build.project, nil) - .close(new_build) - - build.pipeline.mark_as_processable_after_stage(build.stage_idx) - new_build + def retry(build, current_user) + Ci::RetryBuildService + .new(build.project, current_user) + .execute(build) end end @@ -136,7 +113,7 @@ module Ci project.builds_enabled? && commands.present? && manual? && skipped? end - def play(current_user = nil) + def play(current_user) # Try to queue a current build if self.enqueue self.update(user: current_user) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bbc358adb83..dc4590a9923 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -214,21 +214,17 @@ module Ci def cancel_running Gitlab::OptimisticLocking.retry_lock( statuses.cancelable) do |cancelable| - cancelable.each(&:cancel) + cancelable.find_each(&:cancel) end end - def retry_failed(user) - Gitlab::OptimisticLocking.retry_lock( - builds.latest.failed_or_canceled) do |failed_or_canceled| - failed_or_canceled.select(&:retryable?).each do |build| - Ci::Build.retry(build, user) - end - end + def retry_failed(current_user) + Ci::RetryPipelineService.new(project, current_user) + .execute(self) end def mark_as_processable_after_stage(stage_idx) - builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) + builds.skipped.after_stage(stage_idx).find_each(&:process) end def latest? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ed1843ba005..07a086b0aca 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -22,8 +22,6 @@ module Ci scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :ordered, ->() { order(id: :desc) } - after_save :tick_runner_queue, if: :form_editable_changed? - scope :owned_or_shared, ->(project_id) do joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) @@ -40,6 +38,8 @@ module Ci acts_as_taggable + after_destroy :cleanup_runner_queue + # Searches for runners matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -147,14 +147,14 @@ module Ci private - def runner_queue_key - "runner:build_queue:#{self.token}" + def cleanup_runner_queue + Gitlab::Redis.with do |redis| + redis.del(runner_queue_key) + end end - def form_editable_changed? - FORM_EDITABLE.any? do |editable| - public_send("#{editable}_changed?") - end + def runner_queue_key + "runner:build_queue:#{self.token}" end def tag_constraints diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9547c57b2ae..99a6326309d 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base where(id: max_id.group(:name, :commit_id)) end - scope :retried, -> { where.not(id: latest) } - scope :ordered, -> { order(:name) } - scope :failed_but_allowed, -> do where(allow_failure: true, status: [:failed, :canceled]) end @@ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base false, all_state_names - [:failed, :canceled]) end + scope :retried, -> { where.not(id: latest) } + scope :ordered, -> { order(:name) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } + scope :after_stage, -> (index) { where('stage_idx > ?', index) } state_machine :status do event :enqueue do diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 423ae98a60e..79adc77c9e4 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -22,6 +22,10 @@ module Spammable delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true end + def submittable_as_spam_by?(current_user) + current_user && current_user.admin? && submittable_as_spam? + end + def submittable_as_spam? if user_agent_detail user_agent_detail.submittable? && current_application_settings.akismet_enabled diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index a03605d01fb..86d271a3f69 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -30,5 +30,9 @@ module ChatMessage def attachment_color '#345' end + + def link(text, url) + "[#{text}](#{url})" + end end end diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb index 53e35cb21bf..c776e0a20c4 100644 --- a/app/models/project_services/chat_message/build_message.rb +++ b/app/models/project_services/chat_message/build_message.rb @@ -7,7 +7,11 @@ module ChatMessage attr_reader :project_name attr_reader :project_url attr_reader :user_name + attr_reader :user_url attr_reader :duration + attr_reader :stage + attr_reader :build_id + attr_reader :build_name def initialize(params) @sha = params[:sha] @@ -17,7 +21,11 @@ module ChatMessage @project_url = params[:project_url] @status = params[:commit][:status] @user_name = params[:commit][:author_name] + @user_url = params[:commit][:author_url] @duration = params[:commit][:duration] + @stage = params[:build_stage] + @build_name = params[:build_name] + @build_id = params[:build_id] end def pretext @@ -35,7 +43,19 @@ module ChatMessage private def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}" + end + + def build_url + "#{project_url}/builds/#{build_id}" + end + + def build_link + link(build_name, build_url) + end + + def user_link + link(user_name, user_url) end def format(string) @@ -64,11 +84,11 @@ module ChatMessage end def branch_link - "[#{ref}](#{branch_url})" + link(ref, branch_url) end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def commit_url @@ -76,7 +96,7 @@ module ChatMessage end def commit_link - "[#{Commit.truncate_sha(sha)}](#{commit_url})" + link(Commit.truncate_sha(sha), commit_url) end end end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 14fd64e5332..b96aca47e65 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -55,11 +55,11 @@ module ChatMessage end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def issue_link - "[#{issue_title}](#{issue_url})" + link(issue_title, issue_url) end def issue_title diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index ab5e8b24167..5e5efca7bec 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -42,7 +42,7 @@ module ChatMessage end def project_link - "[#{project_name}](#{project_url})" + link(project_name, project_url) end def merge_request_message @@ -50,7 +50,7 @@ module ChatMessage end def merge_request_link - "[merge request !#{merge_request_id}](#{merge_request_url})" + link("merge request !#{merge_request_id}", merge_request_url) end def merge_request_url diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index ca1d7207034..552113bac29 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -3,10 +3,9 @@ module ChatMessage attr_reader :message attr_reader :user_name attr_reader :project_name - attr_reader :project_link + attr_reader :project_url attr_reader :note attr_reader :note_url - attr_reader :title def initialize(params) params = HashWithIndifferentAccess.new(params) @@ -69,15 +68,15 @@ module ChatMessage end def description_message - [{ text: format(@note), color: attachment_color }] + [{ text: format(note), color: attachment_color }] end def project_link - "[#{@project_name}](#{@project_url})" + link(project_name, project_url) end def commented_on_message(target, title) - @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" + @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*" end end end diff --git a/app/models/user.rb b/app/models/user.rb index ad997ce2b13..f614eb66e1f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,7 +21,6 @@ class User < ActiveRecord::Base default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false - default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index a559d0850c4..69bf693de8d 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -2,6 +2,7 @@ class AnalyticsStageEntity < Grape::Entity include EntityDateHelper expose :title + expose :legend expose :description expose :median, as: :value do |stage| diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 1a2bad77a02..fa45506317e 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -1,4 +1,5 @@ class BaseService + include Gitlab::Allowable include Gitlab::CurrentSettings attr_accessor :project, :current_user, :params @@ -7,10 +8,6 @@ class BaseService @project, @current_user, @params = project, user, params.dup end - def can?(object, action, subject) - Ability.allowed?(object, action, subject) - end - def notification_service NotificationService.new end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb new file mode 100644 index 00000000000..4b47ee489cf --- /dev/null +++ b/app/services/ci/retry_build_service.rb @@ -0,0 +1,42 @@ +module Ci + class RetryBuildService < ::BaseService + CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name + allow_failure stage stage_idx trigger_request + yaml_variables when environment coverage_regex] + .freeze + + REJECT_ATTRIBUTES = %i[id status user token coverage trace runner + artifacts_file artifacts_metadata artifacts_size + created_at updated_at started_at finished_at + queued_at erased_by erased_at].freeze + + IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url + deploy job_id description].freeze + + def execute(build) + reprocess(build).tap do |new_build| + build.pipeline.mark_as_processable_after_stage(build.stage_idx) + + new_build.enqueue! + + MergeRequests::AddTodoWhenBuildFailsService + .new(project, current_user) + .close(new_build) + end + end + + def reprocess(build) + unless can?(current_user, :update_build, build) + raise Gitlab::Access::AccessDeniedError + end + + attributes = CLONE_ATTRIBUTES.map do |attribute| + [attribute, build.send(attribute)] + end + + attributes.push([:user, current_user]) + + project.builds.create(Hash[attributes]) + end + end +end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb new file mode 100644 index 00000000000..2c5e130e5aa --- /dev/null +++ b/app/services/ci/retry_pipeline_service.rb @@ -0,0 +1,22 @@ +module Ci + class RetryPipelineService < ::BaseService + def execute(pipeline) + unless can?(current_user, :update_pipeline, pipeline) + raise Gitlab::Access::AccessDeniedError + end + + pipeline.builds.failed_or_canceled.find_each do |build| + next unless build.retryable? + + Ci::RetryBuildService.new(project, current_user) + .reprocess(build) + end + + MergeRequests::AddTodoWhenBuildFailsService + .new(project, current_user) + .close_all(pipeline) + + pipeline.process! + end + end +end diff --git a/app/services/ci/update_runner_service.rb b/app/services/ci/update_runner_service.rb new file mode 100644 index 00000000000..450ee7da1c9 --- /dev/null +++ b/app/services/ci/update_runner_service.rb @@ -0,0 +1,15 @@ +module Ci + class UpdateRunnerService + attr_reader :runner + + def initialize(runner) + @runner = runner + end + + def update(params) + runner.update(params).tap do |updated| + runner.tick_runner_queue if updated + end + end + end +end diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 12a8415d9a5..727768b1a39 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -18,5 +18,11 @@ module MergeRequests todo_service.merge_request_build_retried(merge_request) end end + + def close_all(pipeline) + pipeline_merge_requests(pipeline) do |merge_request| + todo_service.merge_request_build_retried(merge_request) + end + end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 177b714b734..3da1b657a41 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -18,7 +18,7 @@ module MergeRequests @source = find_merge_source unless @source - log_merge_error('No source for merge', save_message_on_model: true) + return log_merge_error('No source for merge', save_message_on_model: true) end merge_request.in_locked_state do diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index a08c6fcd94b..9716a1780a9 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -17,8 +17,6 @@ module Projects def execute return false unless can?(current_user, :remove_project, project) - project.team.truncate - repo_path = project.path_with_namespace wiki_path = repo_path + '.wiki' @@ -30,6 +28,7 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute Project.transaction do + project.team.truncate project.destroy! unless remove_registry_tags diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8ab943f4639..ad86b4f9f42 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -170,16 +170,20 @@ class TodoService # When user marks some todos as done def mark_todos_as_done(todos, current_user) - mark_todos_as_done_by_ids(todos.select(&:id), current_user) + update_todos_state_by_ids(todos.select(&:id), current_user, :done) end def mark_todos_as_done_by_ids(ids, current_user) - todos = current_user.todos.where(id: ids) + update_todos_state_by_ids(ids, current_user, :done) + end - # Only return those that are not really on that state - marked_todos = todos.where.not(state: :done).update_all(state: :done) - current_user.update_todos_count_cache - marked_todos + # When user marks some todos as pending + def mark_todos_as_pending(todos, current_user) + update_todos_state_by_ids(todos.select(&:id), current_user, :pending) + end + + def mark_todos_as_pending_by_ids(ids, current_user) + update_todos_state_by_ids(ids, current_user, :pending) end # When user marks an issue as todo @@ -194,6 +198,15 @@ class TodoService private + def update_todos_state_by_ids(ids, current_user, state) + todos = current_user.todos.where(id: ids) + + # Only return those that are not really on that state + marked_todos = todos.where.not(state: state).update_all(state: state) + current_user.update_todos_count_cache + marked_todos + end + def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index c4b748d0ab8..6c48328da4f 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -12,6 +12,7 @@ %th.wide Message %th Action = render @abuse_reports + = paginate @abuse_reports, theme: 'gitlab' - else .empty-state .text-center diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 816035ec442..749c74b8110 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -192,7 +192,7 @@ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :max_pages_size, class: 'form-control' - .help-block Zero for unlimited + .help-block 0 for unlimited %fieldset %legend Continuous Integration @@ -525,7 +525,7 @@ = f.number_field :terminal_max_session_time, class: 'form-control' .help-block Maximum time for web terminal websocket connection (in seconds). - Set to 0 for unlimited time. + 0 for unlimited. .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 4f982a6e369..ac36bb5bb17 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -35,7 +35,7 @@ .clearfix %p %i.fa.fa-exclamation-circle - If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. + If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'. %p %i.fa.fa-exclamation-circle If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 721bc77cc2f..d725e477044 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -56,7 +56,7 @@ = submit_tag 'Search', class: 'btn' .pull-right.light - Runners with last contact less than a minute ago: #{@active_runners_cnt} + Runners with last contact more than a minute ago: #{@active_runners_cnt} %br diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 605bfd0cf8d..dc2d924f212 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -31,6 +31,9 @@ - if todo.pending? .todo-actions - = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do + = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do Done = icon('spinner spin') + = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do + Undo + = icon('spinner spin') diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 64ca3c32e01..efd13aabf20 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -3,11 +3,9 @@ .event-title %span.author_name= link_to_author event %span.pushed #{event.action_name} #{event.ref_type} - - if event.rm_ref? - %strong= event.ref_name - - else - %strong - = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title) + %strong + - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) + = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link = render "events/event_scope", event: event diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 54d02ee8e4b..1717ed6b365 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,24 +1,7 @@ -.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - .sidebar-wrapper.nicescroll - .sidebar-action-buttons - .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" } - %span.sr-only Toggle navigation - = icon('bars') - - %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } } - %span.sr-only Toggle navigation pinning - = icon('fw thumb-tack') - - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' - - else - = render 'layouts/nav/explore' - +.page-with-sidebar{ class: page_gutter_class } - if defined?(nav) && nav .layout-nav - .container-fluid + %div{ class: container_class } = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 248d439cd05..19bd9b6d5c9 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" - %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data = render "layouts/header/default", title: header_title diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 59082ce5fd5..60b9b8bdbc4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -2,9 +2,15 @@ %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content - %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" } - %span.sr-only Toggle navigation - = icon('bars') + .dropdown.global-dropdown + %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.sr-only Toggle navigation + = icon('bars') + .dropdown-menu-nav.global-dropdown-menu + - if current_user + = render 'layouts/nav/dashboard' + - else + = render 'layouts/nav/explore' %button.navbar-toggle{ type: 'button' } %span.sr-only Toggle navigation = icon('ellipsis-v') @@ -55,13 +61,12 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - - %h1.title= title - .header-logo = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo + %h1.title= title + = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 205d23178d2..5d4178f03d7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,41 +1,39 @@ -.nav-sidebar - %ul.nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do - = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - %span - Projects - = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - %span - Activity - - if koding_enabled? - = nav_link(controller: :koding) do - = link_to koding_path, title: 'Koding' do - %span - Koding - = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do - = link_to dashboard_groups_path, title: 'Groups' do - %span - Groups - = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, title: 'Milestones' do - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - %span - Issues - %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - %span - Merge Requests - %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) - = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, title: 'Snippets' do +%ul + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + %span + Projects + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + %span + Activity + - if koding_enabled? + = nav_link(controller: :koding) do + = link_to koding_path, title: 'Koding' do %span - Snippets - - = link_to help_path, title: 'About GitLab CE', class: 'about-gitlab' do + Koding + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do + = link_to dashboard_groups_path, title: 'Groups' do + %span + Groups + = nav_link(controller: 'dashboard/milestones') do + = link_to dashboard_milestones_path, title: 'Milestones' do + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + %span + Issues + (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))}) + = nav_link(path: 'dashboard#merge_requests') do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do + %span + Merge Requests + (#{number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))}) + = nav_link(controller: 'dashboard/snippets') do + = link_to dashboard_snippets_path, title: 'Snippets' do %span - About GitLab CE + Snippets + %li.divider + %li + = link_to "About GitLab CE", help_path, title: 'About GitLab CE', class: 'about-gitlab' diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index e5bda7b3a6f..3a1fcd00e9c 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -1,4 +1,4 @@ -%ul.nav.nav-sidebar +%ul = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = link_to explore_root_path, title: 'Projects' do %span diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index feadd863b00..df0a0212f3d 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -4,19 +4,6 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Application theme - %p - This setting allows you to customize the appearance of the site, e.g. the sidebar. - .col-lg-9.application-theme - - Gitlab::Themes.each do |theme| - = label_tag do - .preview{ class: theme.css_class } - = f.radio_button :theme_id, theme.id - = theme.name - .col-sm-12 - %hr - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 Syntax highlighting theme %p This setting allow you to customize the appearance of the syntax. diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 8966dd3fd86..431ab9d052b 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,7 +1,3 @@ -// Remove body class for any previous theme, re-add current one -$('body').removeClass('<%= Gitlab::Themes.body_classes %>') -$('body').addClass('<%= user_application_theme %>') - // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper .container-fluid').removeClass('container-limited') diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 3b1a2e54ec2..d1f7f65bf53 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -25,6 +25,6 @@ = link_to raw(line_new), "##{line_new}" = line_content - - if @form.unfold? && @form.bottom? && @form.to < @blob.loc + - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size %tr.line_holder{ id: @form.to, class: line_class } = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index b87b79b170e..5c38b5ad9c0 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -15,10 +15,13 @@ %a.click-to-expand Click to expand it. - elsif diff_file.diff_lines.length > 0 + - total_lines = 0 + - if blob.lines.any? + - total_lines = blob.lines.last.chomp == '' ? blob.lines.size - 1 : blob.lines.size - if diff_view == :parallel - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob + = render "projects/diffs/parallel_view", diff_file: diff_file, total_lines: total_lines - else - = render "projects/diffs/text_file", diff_file: diff_file + = render "projects/diffs/text_file", diff_file: diff_file, total_lines: total_lines - else - if diff_file.mode_changed? .nothing-here-block File mode changed diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 074f1f634ae..997bf0fc560 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -43,7 +43,8 @@ - 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 && diff_file.diff_lines.any? + - if !diff_file.new_file && !diff_file.deleted_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 + - if last_line.new_pos < total_lines + %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 2eea1db169a..ebd1a914ee7 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -10,7 +10,8 @@ as: :line, locals: { diff_file: diff_file, discussions: discussions } - - if !diff_file.new_file && diff_file.highlighted_diff_lines.any? + - if !diff_file.new_file && !diff_file.deleted_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 + - if last_line.new_pos < total_lines + %tr.line_holder + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index d3eb3b7055b..069f3d97943 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -40,7 +40,7 @@ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - - if @issue.submittable_as_spam? && current_user.admin? + - if @issue.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' @@ -50,7 +50,7 @@ - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - - if @issue.submittable_as_spam? && current_user.admin? + - if @issue.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 1b08165c14c..a73e8f345e0 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -71,7 +71,7 @@ - if note_editable .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } #{note.note} - %textarea.hidden.js-task-list-field.original-task-list= note.note + %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index a6cd2d83bd5..e0c972aa2fb 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -7,9 +7,9 @@ = commit_author_link(@commit) .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - - if @pipeline.builds.latest.failed.any?(&:retryable?) + - if @pipeline.retryable? = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post - - if @pipeline.builds.running_or_pending.any? + - if @pipeline.cancelable? = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - if @commit diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 80d4081dd7b..f7419728719 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,69 +13,70 @@ = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - %nav.project-stats{ class: container_class } - %ul.nav - %li - = link_to project_files_path(@project) do - Files (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - - if default_project_view != 'readme' && @repository.readme + .project-stats-container{ class: container_class } + %nav.project-stats + %ul.nav %li - = link_to 'Readme', readme_path(@project) - - - if @repository.changelog + = link_to project_files_path(@project) do + Files (#{storage_counter(@project.statistics.total_repository_size)}) %li - = link_to 'Changelog', changelog_path(@project) - - - if @repository.license_blob + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if @repository.gitlab_ci_yml - %li - = link_to 'CI configuration', ci_configuration_path(@project) + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) + + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) + + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) + + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to 'Set up Koding', add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do - Set up auto deploy + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - - if @repository.commit - .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project + - if @repository.commit + .project-last-commit + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index dde2e2b644d..34ee4ff1937 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -10,7 +10,7 @@ - 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? + - if @snippet.submittable_as_spam_by?(current_user) = 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 @@ -31,6 +31,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ead9b84b991..1744a597c51 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,6 +1,4 @@ - label_css_id = dom_id(label) -- open_issues_count = label.open_issues_count(current_user) -- open_merge_requests_count = label.open_merge_requests_count(current_user) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] @@ -15,10 +13,10 @@ %ul %li = link_to_label(label, subject: subject, type: :merge_request) do - = pluralize open_merge_requests_count, 'merge request' + view merge requests %li = link_to_label(label, subject: subject) do - = pluralize open_issues_count, 'open issue' + view open issues - if current_user && defined?(@project) %li.label-subscription - if label.is_a?(ProjectLabel) @@ -40,9 +38,9 @@ .pull-right.hidden-xs.hidden-sm.hidden-md = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do - = pluralize open_merge_requests_count, 'merge request' + view merge requests = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do - = pluralize open_issues_count, 'open issue' + view open issues - if current_user && defined?(@project) .label-subscription.inline diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 31eb07ca666..a93cbd1041f 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -3,11 +3,11 @@ - panel_class = primary ? 'panel-primary' : 'panel-default' .panel{ class: panel_class } - .panel-heading.split - .left + .panel-heading + .title = title - if show_counter - .right + .counter = number_with_delimiter(issuables.size) - class_prefix = dom_class(issuables).pluralize diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 855a995afa9..a7f118d3f7d 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -9,7 +9,7 @@ Delete = 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? + - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -28,6 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/changelogs/unreleased/21240_snippets_line_ending.yml b/changelogs/unreleased/21240_snippets_line_ending.yml new file mode 100644 index 00000000000..880fdd2c9ed --- /dev/null +++ b/changelogs/unreleased/21240_snippets_line_ending.yml @@ -0,0 +1,4 @@ +--- +title: Download snippets with LF line-endings by default +merge_request: 8999 +author: diff --git a/changelogs/unreleased/22018-api-milestone-merge-requests.yml b/changelogs/unreleased/22018-api-milestone-merge-requests.yml new file mode 100644 index 00000000000..ccad2ec838c --- /dev/null +++ b/changelogs/unreleased/22018-api-milestone-merge-requests.yml @@ -0,0 +1,4 @@ +--- +title: Adds API endpoint to fetch all merge request for a single milestone +merge_request: +author: Joren De Groof diff --git a/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml new file mode 100644 index 00000000000..628db8a5419 --- /dev/null +++ b/changelogs/unreleased/23819-fix-milestone-counters-to-top-right-of-panel-headings.yml @@ -0,0 +1,4 @@ +--- +title: Fix position of counter in milestone panels +merge_request: 7842 +author: Andrew Smith (EspadaV8) diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml deleted file mode 100644 index 05fbd8f0bf2..00000000000 --- a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms -merge_request: 8752 -author: diff --git a/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml new file mode 100644 index 00000000000..e9d46f6b122 --- /dev/null +++ b/changelogs/unreleased/25465-todo-done-clicking-is-kind-of-unsafe.yml @@ -0,0 +1,4 @@ +--- +title: Todo done clicking is kind of unusable +merge_request: 8691 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/26206-fix-download-dropdown.yml b/changelogs/unreleased/26206-fix-download-dropdown.yml new file mode 100644 index 00000000000..a6c101375bb --- /dev/null +++ b/changelogs/unreleased/26206-fix-download-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Set dropdown height fixed to 250px and make it scrollable +merge_request: 9063 +author: diff --git a/changelogs/unreleased/26315-unify-labels-filter-behavior.yml b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml new file mode 100644 index 00000000000..cd2f40c94fe --- /dev/null +++ b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml @@ -0,0 +1,4 @@ +--- +title: Unify issues search behavior by always filtering when ALL labels matches +merge_request: 8849 +author: diff --git a/changelogs/unreleased/26379-iid-param.yml b/changelogs/unreleased/26379-iid-param.yml new file mode 100644 index 00000000000..ac743e68d6f --- /dev/null +++ b/changelogs/unreleased/26379-iid-param.yml @@ -0,0 +1,4 @@ +--- +title: add :iids param to IssuableFinder (resolve technical dept) +merge_request: 9222 +author: mhasbini diff --git a/changelogs/unreleased/26500-informative-slack-notifications.yml b/changelogs/unreleased/26500-informative-slack-notifications.yml new file mode 100644 index 00000000000..342235424f4 --- /dev/null +++ b/changelogs/unreleased/26500-informative-slack-notifications.yml @@ -0,0 +1,4 @@ +--- +title: Add user & build links in Slack Notifications +merge_request: 8641 +author: Poornima M diff --git a/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml new file mode 100644 index 00000000000..182a9ae126b --- /dev/null +++ b/changelogs/unreleased/26824-diff-unfold-link-is-still-visible-when-there-are-no-lines-to-unfold.yml @@ -0,0 +1,5 @@ +--- +title: prevent diff unfolding link from appearing when there are no more lines to + show +merge_request: 8761 +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 deleted file mode 100644 index 7b307b501f4..00000000000 --- a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index -merge_request: 8956 -author: diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml deleted file mode 100644 index 8f061a34ac0..00000000000 --- a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix filtered search user autocomplete for gitlab instances that are hosted - on a subdirectory -merge_request: 8891 -author: diff --git a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml b/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml deleted file mode 100644 index 6e9192cb632..00000000000 --- a/changelogs/unreleased/27480-deploy_keys_should_not_show_up_in_users_keys_list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Do not display deploy keys in user's own ssh keys list -merge_request: 9024 -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 deleted file mode 100644 index bc990c66866..00000000000 --- a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix wrong call to ProjectCacheWorker.perform -merge_request: 8910 -author: diff --git a/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml new file mode 100644 index 00000000000..52b7e96682d --- /dev/null +++ b/changelogs/unreleased/27883-autocomplete-seems-to-not-trigger-when-at-character-is-part-of-an-autocompleted-text.yml @@ -0,0 +1,4 @@ +--- +title: Trigger autocomplete after selecting a slash command +merge_request: 9117 +author: diff --git a/changelogs/unreleased/27920-both-wip-messages-showing.yml b/changelogs/unreleased/27920-both-wip-messages-showing.yml new file mode 100644 index 00000000000..497fda8c8ba --- /dev/null +++ b/changelogs/unreleased/27920-both-wip-messages-showing.yml @@ -0,0 +1,4 @@ +--- +title: Dispatch needed JS when creating a new MR in diff view +merge_request: +author: diff --git a/changelogs/unreleased/27934-left-align-nav.yml b/changelogs/unreleased/27934-left-align-nav.yml new file mode 100644 index 00000000000..6c45e4ce175 --- /dev/null +++ b/changelogs/unreleased/27934-left-align-nav.yml @@ -0,0 +1,4 @@ +--- +title: Left align navigation +merge_request: +author: diff --git a/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml new file mode 100644 index 00000000000..1b2e678bbed --- /dev/null +++ b/changelogs/unreleased/28059-add-pagination-to-admin-abuse-reports.yml @@ -0,0 +1,4 @@ +--- +title: Restore pagination to admin abuse reports +merge_request: +author: diff --git a/changelogs/unreleased/28082-deleted-branch-event-404.yml b/changelogs/unreleased/28082-deleted-branch-event-404.yml new file mode 100644 index 00000000000..e989ca34784 --- /dev/null +++ b/changelogs/unreleased/28082-deleted-branch-event-404.yml @@ -0,0 +1,4 @@ +--- +title: Stop linking to deleted Branches in Activity tabs +merge_request: 9203 +author: Jan Christophersen diff --git a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml b/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml deleted file mode 100644 index cd61c38e1bc..00000000000 --- a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show merge errors in merge request widget -merge_request: 9229 -author: diff --git a/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml new file mode 100644 index 00000000000..df2478a3f28 --- /dev/null +++ b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml @@ -0,0 +1,4 @@ +--- +title: Pick up option from GDK to disable webpack dev server livereload +merge_request: +author: diff --git a/changelogs/unreleased/28236-browse-button-dropping.yml b/changelogs/unreleased/28236-browse-button-dropping.yml new file mode 100644 index 00000000000..3a3d755f40c --- /dev/null +++ b/changelogs/unreleased/28236-browse-button-dropping.yml @@ -0,0 +1,4 @@ +--- +title: Increase right side of file header to button stays on same line +merge_request: +author: diff --git a/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml new file mode 100644 index 00000000000..b97e9a59b2a --- /dev/null +++ b/changelogs/unreleased/28303-change-development-tanuki-favicon-colors-to-match-logo.yml @@ -0,0 +1,4 @@ +--- +title: Change development tanuki favicon colors to match logo color order +merge_request: +author: diff --git a/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml new file mode 100644 index 00000000000..fed02139a5c --- /dev/null +++ b/changelogs/unreleased/28329-allow-slash-in-slash-command-args.yml @@ -0,0 +1,4 @@ +--- +title: Allow slashes in slash command arguments +merge_request: +author: diff --git a/changelogs/unreleased/28353-little-grammar-issue.yml b/changelogs/unreleased/28353-little-grammar-issue.yml new file mode 100644 index 00000000000..10bdb17b266 --- /dev/null +++ b/changelogs/unreleased/28353-little-grammar-issue.yml @@ -0,0 +1,4 @@ +--- +title: Fix grammer issue in admin/runners +merge_request: +author: diff --git a/changelogs/unreleased/9381-authentiq-backchannel-logout.yml b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml new file mode 100644 index 00000000000..4dbf36cd096 --- /dev/null +++ b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml @@ -0,0 +1,4 @@ +--- +title: Adds remote logout functionality to the Authentiq OAuth provider +merge_request: 9381 +author: Alexandros Keramidas diff --git a/changelogs/unreleased/add-yarn-documentation.yml b/changelogs/unreleased/add-yarn-documentation.yml new file mode 100644 index 00000000000..5bcc01ac177 --- /dev/null +++ b/changelogs/unreleased/add-yarn-documentation.yml @@ -0,0 +1,4 @@ +--- +title: add rake tasks to handle yarn dependencies and update documentation +merge_request: 9316 +author: diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml deleted file mode 100644 index 8a9e29109a8..00000000000 --- a/changelogs/unreleased/api-fix-files.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Fix file downloading' -merge_request: Robert Schilling -author: 8267 diff --git a/changelogs/unreleased/api-post-block.yml b/changelogs/unreleased/api-post-block.yml new file mode 100644 index 00000000000..dfc61ffa9e3 --- /dev/null +++ b/changelogs/unreleased/api-post-block.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Use POST to (un)block a user' +merge_request: 9371 +author: Robert Schilling diff --git a/changelogs/unreleased/api-remove-deploy-key-disable.yml b/changelogs/unreleased/api-remove-deploy-key-disable.yml new file mode 100644 index 00000000000..f471ad2aa20 --- /dev/null +++ b/changelogs/unreleased/api-remove-deploy-key-disable.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove `DELETE projects/:id/deploy_keys/:key_id/disable`' +merge_request: 9365 +author: Robert Schilling diff --git a/changelogs/unreleased/api-star-restful.yml b/changelogs/unreleased/api-star-restful.yml new file mode 100644 index 00000000000..3e7de8cd822 --- /dev/null +++ b/changelogs/unreleased/api-star-restful.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`' +merge_request: 9328 +author: Robert Schilling diff --git a/changelogs/unreleased/artifactsdoc.yml b/changelogs/unreleased/artifactsdoc.yml new file mode 100644 index 00000000000..4ef32d5256f --- /dev/null +++ b/changelogs/unreleased/artifactsdoc.yml @@ -0,0 +1,4 @@ +--- +title: Added documentation for permalinks to most recent build artifacts. +merge_request: 8934 +author: Christian Godenschwager diff --git a/changelogs/unreleased/change_queue_weight.yml b/changelogs/unreleased/change_queue_weight.yml new file mode 100644 index 00000000000..e4c650e8f79 --- /dev/null +++ b/changelogs/unreleased/change_queue_weight.yml @@ -0,0 +1,4 @@ +--- +title: Increase process_commit queue weight from 2 to 3 +merge_request: 9326 +author: blackst0ne diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml deleted file mode 100644 index fb589a053c0..00000000000 --- a/changelogs/unreleased/dont-delete-assigned-issuables.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't delete assigned MRs/issues when user is deleted -merge_request: -author: diff --git a/changelogs/unreleased/dynamic-project-title-fixture.yml b/changelogs/unreleased/dynamic-project-title-fixture.yml new file mode 100644 index 00000000000..2404cbb891c --- /dev/null +++ b/changelogs/unreleased/dynamic-project-title-fixture.yml @@ -0,0 +1,4 @@ +--- +title: Replace static fixture for project_title_spec.js +merge_request: 9175 +author: winniehell diff --git a/changelogs/unreleased/feature-github-find-users-by-email.yml b/changelogs/unreleased/feature-github-find-users-by-email.yml new file mode 100644 index 00000000000..1503cf2b9f7 --- /dev/null +++ b/changelogs/unreleased/feature-github-find-users-by-email.yml @@ -0,0 +1,4 @@ +--- +title: GitHub Importer - Find users based on GitHub email address +merge_request: 8958 +author: diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml deleted file mode 100644 index 43b3b9bf96e..00000000000 --- a/changelogs/unreleased/fix-anchor-scrolling.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken anchor links when special characters are used -merge_request: 8961 -author: Andrey Krivko diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml new file mode 100644 index 00000000000..49e243ca6bb --- /dev/null +++ b/changelogs/unreleased/fix-gb-pipeline-retry-builds-started.yml @@ -0,0 +1,4 @@ +--- +title: Fix CI/CD pipeline retry and take stages order into account +merge_request: 9021 +author: diff --git a/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml new file mode 100644 index 00000000000..d747e0e63a3 --- /dev/null +++ b/changelogs/unreleased/fix-gb-pipeline-retry-cancel-buttons-consistency.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline retry and cancel buttons on pipeline details page +merge_request: 9225 +author: diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml new file mode 100644 index 00000000000..e09d03bb608 --- /dev/null +++ b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml @@ -0,0 +1,4 @@ +--- +title: fix incorrect sidekiq concurrency count in admin background page +merge_request: +author: wendy0402 diff --git a/changelogs/unreleased/gfm-autocomplete-fixes.yml b/changelogs/unreleased/gfm-autocomplete-fixes.yml new file mode 100644 index 00000000000..737e2ad5234 --- /dev/null +++ b/changelogs/unreleased/gfm-autocomplete-fixes.yml @@ -0,0 +1,4 @@ +--- +title: Fix errors in slash commands matcher, add simple test coverage +merge_request: +author: YarNayar diff --git a/changelogs/unreleased/only-yield-valid-reference-matches.yml b/changelogs/unreleased/only-yield-valid-reference-matches.yml new file mode 100644 index 00000000000..95da3cc56fd --- /dev/null +++ b/changelogs/unreleased/only-yield-valid-reference-matches.yml @@ -0,0 +1,4 @@ +--- +title: Only yield valid references in ReferenceFilter.references_in +merge_request: +author: diff --git a/changelogs/unreleased/paginate-all-the-things.yml b/changelogs/unreleased/paginate-all-the-things.yml new file mode 100644 index 00000000000..52f23ba52a9 --- /dev/null +++ b/changelogs/unreleased/paginate-all-the-things.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Paginate all endpoints that return an array' +merge_request: 8606 +author: Robert Schilling diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml deleted file mode 100644 index a94bcdaa9a3..00000000000 --- a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refresh authorizations when transferring projects -merge_request: -author: diff --git a/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml new file mode 100644 index 00000000000..b75b4644ba3 --- /dev/null +++ b/changelogs/unreleased/remove-issue-and-mr-counts-from-labels-page.yml @@ -0,0 +1,4 @@ +--- +title: Remove issue and MR counts from labels index +merge_request: +author: diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml deleted file mode 100644 index f42aa6fae79..00000000000 --- a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't use backup Active Record connections for Sidekiq -merge_request: -author: diff --git a/changelogs/unreleased/task_list_refactor.yml b/changelogs/unreleased/task_list_refactor.yml new file mode 100644 index 00000000000..68942dadaa8 --- /dev/null +++ b/changelogs/unreleased/task_list_refactor.yml @@ -0,0 +1,4 @@ +--- +title: Deduplicate markdown task lists +merge_request: +author: diff --git a/changelogs/unreleased/updated-pages-0-3-1.yml b/changelogs/unreleased/updated-pages-0-3-1.yml new file mode 100644 index 00000000000..8622b823c86 --- /dev/null +++ b/changelogs/unreleased/updated-pages-0-3-1.yml @@ -0,0 +1,4 @@ +--- +title: Update GitLab Pages to v0.3.1 +merge_request: +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cc1af77a1de..a82ff605a70 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,14 +76,6 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace - ## Default theme ID - ## 1 - Graphite - ## 2 - Charcoal - ## 3 - Green - ## 4 - Gray - ## 5 - Violet - ## 6 - Blue - # default_theme: 2 # default: 2 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. @@ -611,4 +603,4 @@ test: admin_group: '' staging: - <<: *base
\ No newline at end of file + <<: *base diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ab59394cb0c..3f716dd8833 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -183,7 +183,6 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 10 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? -Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb deleted file mode 100644 index d252e403102..00000000000 --- a/config/initializers/4_ci_app.rb +++ /dev/null @@ -1,8 +0,0 @@ -module GitlabCi - VERSION = Gitlab::VERSION - REVISION = Gitlab::REVISION - - def self.config - Settings - end -end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a8afc36fc78..738dbeefc11 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -240,6 +240,17 @@ Devise.setup do |config| true end end + if provider['name'] == 'authentiq' + provider['args'][:remote_sign_out_handler] = lambda do |request| + authentiq_session = request.params['sid'] + if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session) + Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session) + true + else + false + end + end + end if provider['name'] == 'shibboleth' provider['args'][:fail_with_empty_uid] = true diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index fb20c63bc63..adc3ad207cc 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -14,6 +14,9 @@ resource :dashboard, controller: 'dashboard', only: [] do collection do delete :destroy_all end + member do + patch :restore + end end resources :projects, only: [:index] do diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 56bf4e6b1de..97620cc9c7f 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -21,7 +21,7 @@ - [post_receive, 5] - [merge, 5] - [update_merge_requests, 3] - - [process_commit, 2] + - [process_commit, 3] - [new_note, 2] - [build, 2] - [pipeline, 2] diff --git a/config/webpack.config.js b/config/webpack.config.js index 01c1a5bfb99..15899993874 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -10,6 +10,7 @@ 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 DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), @@ -60,12 +61,6 @@ var config = { 'stage-2' ] } - }, - { - test: /\.(js|es6)$/, - exclude: /node_modules/, - loader: 'imports-loader', - options: 'this=>window' } ] }, @@ -80,9 +75,6 @@ var config = { modules: false, assets: true }), - new CompressionPlugin({ - asset: '[path].gz[query]', - }), new webpack.IgnorePlugin(/moment/, /pikaday/), ], @@ -93,8 +85,7 @@ var config = { 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': 'vue/dist/vue.js', - 'vue-resource$': 'vue-resource/dist/vue-resource.js' + 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', } } } @@ -102,7 +93,7 @@ var config = { if (IS_PRODUCTION) { config.devtool = 'source-map'; config.plugins.push( - new webpack.NoErrorsPlugin(), + new webpack.NoEmitOnErrorsPlugin(), new webpack.LoaderOptionsPlugin({ minimize: true, debug: false @@ -112,6 +103,9 @@ if (IS_PRODUCTION) { }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } + }), + new CompressionPlugin({ + asset: '[path].gz[query]', }) ); } @@ -121,6 +115,7 @@ if (IS_DEV_SERVER) { port: DEV_SERVER_PORT, headers: { 'Access-Control-Allow-Origin': '*' }, stats: 'errors-only', + inline: DEV_SERVER_LIVERELOAD }; config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; } diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb new file mode 100644 index 00000000000..c01753cfbd2 --- /dev/null +++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToUserAgentDetail < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:user_agent_details, [:subject_id, :subject_type]) + end +end diff --git a/db/post_migrate/20170215200045_remove_theme_id_from_users.rb b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb new file mode 100644 index 00000000000..c51646fbe52 --- /dev/null +++ b/db/post_migrate/20170215200045_remove_theme_id_from_users.rb @@ -0,0 +1,9 @@ +class RemoveThemeIdFromUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + remove_column :users, :theme_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 52672406ec6..88aaa6c3c55 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: 20170214111112) do +ActiveRecord::Schema.define(version: 20170215200045) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1218,6 +1218,8 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.datetime "updated_at", null: false end + add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -1238,7 +1240,6 @@ ActiveRecord::Schema.define(version: 20170214111112) do t.string "linkedin", default: "", null: false t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false t.string "bio" t.integer "failed_attempts", default: 0 t.datetime "locked_at" @@ -1351,4 +1352,4 @@ ActiveRecord::Schema.define(version: 20170214111112) do add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" -end
\ No newline at end of file +end diff --git a/doc/README.md b/doc/README.md index 1943d656aa7..2712206373d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,6 +21,7 @@ - [Profile Settings](profile/README.md) - [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. +- [Snippets](user/snippets.md) Snippets allow you to create little bits of code. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [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. diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md index 3f39539da95..fb1a16b0f96 100644 --- a/doc/administration/auth/authentiq.md +++ b/doc/administration/auth/authentiq.md @@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t 5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. -6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. +6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1. 7. Save the configuration file. diff --git a/doc/api/commits.md b/doc/api/commits.md index 3223b82f60a..eaab3e0df3d 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -12,8 +12,8 @@ GET /projects/:id/repository/commits | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | -| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | -| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 284d5f88c55..39afc4b2df5 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -137,7 +137,7 @@ Example response: ## Delete deploy key -Delete a deploy key from a project +Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system. ``` DELETE /projects/:id/deploy_keys/:key_id @@ -156,14 +156,11 @@ Example response: ```json { - "updated_at" : "2015-08-29T12:50:57.259Z", - "key" : "ssh-rsa AAAA...", - "public" : false, - "title" : "My deploy key", - "user_id" : null, - "created_at" : "2015-08-29T12:50:57.259Z", - "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43", - "id" : 13 + "id": 6, + "deploy_key_id": 14, + "project_id": 1, + "created_at" : "2015-08-29T12:50:57.259Z", + "updated_at" : "2015-08-29T12:50:57.259Z" } ``` @@ -190,27 +187,3 @@ Example response: "created_at" : "2015-08-29T12:44:31.550Z" } ``` - -## Disable a deploy key - -Disable a deploy key for a project. Returns the disabled key. - -```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `key_id` | integer | yes | The ID of the deploy key | - -Example response: - -```json -{ - "key" : "ssh-rsa AAAA...", - "id" : 12, - "title" : "My deploy key", - "created_at" : "2015-08-29T12:44:31.550Z" -} -``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 7c0a444d4fa..f40e0938b0f 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -30,7 +30,7 @@ GET /issues?milestone=1.0.0&state=opened | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| -| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -188,7 +188,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened | `id` | integer | yes | The ID of a project | | `iid` | integer | no | Return the issue having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| -| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | diff --git a/doc/api/keys.md b/doc/api/keys.md index b68f08a007d..3b55c2baf56 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -33,7 +33,6 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", - "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 12497acff98..bf7dcc008e9 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -103,3 +103,16 @@ Parameters: - `id` (required) - The ID of a project - `milestone_id` (required) - The ID of a project milestone + +## Get all merge requests assigned to a single milestone + +Gets all merge requests assigned to a single project milestone. + +``` +GET /projects/:id/milestones/:milestone_id/merge_requests +``` + +Parameters: + +- `id` (required) - The ID of a project +- `milestone_id` (required) - The ID of a project milestone
\ No newline at end of file diff --git a/doc/api/projects.md b/doc/api/projects.md index b3136be6731..e9ef03a0c0c 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -609,7 +609,7 @@ Example response: Unstars a given project. Returns status code `304` if the project is not starred. ``` -DELETE /projects/:id/star +POST /projects/:id/unstar ``` | Attribute | Type | Required | Description | @@ -617,7 +617,7 @@ DELETE /projects/:id/star | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" ``` Example response: @@ -1194,4 +1194,4 @@ Parameters: | --------- | ---- | -------- | ----------- | | `query` | string | yes | A string contained in the project name | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | -| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
\ No newline at end of file +| `sort` | string | no | Return requests sorted in `asc` or `desc` order | diff --git a/doc/api/session.md b/doc/api/session.md index f776424023e..d7809716fbe 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -41,7 +41,6 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", - "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index ed3469521fc..852c7ac8ec2 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,7 +72,6 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -105,7 +104,6 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", - "theme_id": 1, "color_scheme_id": 3, "projects_limit": 100, "current_sign_in_at": "2014-03-19T17:54:13Z", @@ -198,7 +196,6 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -323,7 +320,6 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -369,7 +365,6 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, "color_scheme_id": 2, "projects_limit": 100, "current_sign_in_at": "2012-06-02T06:36:55Z", @@ -664,14 +659,14 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann Blocks the specified user. Available only for admin. ``` -PUT /users/:id/block +POST /users/:id/block ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to block an already blocked user by LDAP synchronization. ## Unblock user @@ -679,14 +674,14 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or Unblocks the specified user. Available only for admin. ``` -PUT /users/:id/unblock +POST /users/:id/unblock ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. ### Get user contribution events diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 84ff72bc36c..3f58c098b43 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -13,6 +13,7 @@ changes are in V4: - Project snippets do not return deprecated field `expires_at` - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) - Status 409 returned for POST `project/:id/members` when a member already exists +- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) - `/licences` - `/licences/:key` @@ -24,3 +25,8 @@ changes are in V4: - `/dockerfiles/:key` - Moved `/projects/fork/:id` to `/projects/:id/fork` - Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters +- Return pagination headers for all endpoints that return an array +- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead +- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` +- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) + diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 9d294240d9d..db92a4b0d80 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -91,7 +91,7 @@ total running time should be: ## Badges -Job status and test coverage report badges are available. You can find their +Pipeline status and test coverage report badges are available. You can find their respective link in the [Pipelines settings] page. [jobs]: #jobs diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 5d177eb26ee..1f115059fb8 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use: - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. - [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU]. +## Requesting Approval for Licenses + +Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document. + ## Notes Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL. @@ -96,3 +100,6 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [OSI-GPL]: https://opensource.org/faq#linking-proprietary-code [OSL]: https://opensource.org/licenses/OSL-3.0 [OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL +[Org-Repo]: https://gitlab.com/gitlab-com/organization +[Acceptable-Licenses]: #acceptable-licenses +[Unacceptable-Licenses]: #unacceptable-licenses diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md index 568dedf1669..2d82b09f301 100644 --- a/doc/development/limit_ee_conflicts.md +++ b/doc/development/limit_ee_conflicts.md @@ -2,19 +2,26 @@ This guide contains best-practices for avoiding conflicts between CE and EE. -## Context +## Daily CE Upstream merge -Usually, GitLab Community Edition is merged into the Enterprise Edition once a -week. During these merges, it's very common to get conflicts when some changes -in CE do not apply cleanly to EE. +GitLab Community Edition is merged daily into the Enterprise Edition (look for +the [`CE Upstream` merge requests]). The daily merge is currently done manually +by four individuals. -There are a few things that can help you as a developer to: +**If a developer pings you in a `CE Upstream` merge request for help with +resolving conflicts, please help them because it means that you didn't do your +job to reduce the conflicts nor to ease their resolution in the first place!** -- know when your merge request to CE will conflict when merged to EE -- avoid such conflicts in the first place -- ease future conflict resolutions if conflict is inevitable +To avoid the conflicts beforehand when working on CE, there are a few tools and +techniques that can help you: -## Check the `rake ee_compat_check` in your merge requests +- know what are the usual types of conflicts and how to prevent them +- the CI `rake ee_compat_check` job tells you if you need to open an EE-version + of your CE merge request + +[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream + +## Check the status of the CI `rake ee_compat_check` job For each commit (except on `master`), the `rake ee_compat_check` CI job tries to detect if the current branch's changes will conflict during the CE->EE merge. diff --git a/doc/development/testing.md b/doc/development/testing.md index 761847b2bab..9b545d7f0f1 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -95,6 +95,25 @@ so we need to set some guidelines for their use going forward: [lets-not]: https://robots.thoughtbot.com/lets-not +### Time-sensitive tests + +[Timecop](https://github.com/travisjeffery/timecop) is available in our +Ruby-based tests for verifying things that are time-sensitive. Any test that +exercises or verifies something time-sensitive should make use of Timecop to +prevent transient test failures. + +Example: + +```ruby +it 'is overdue' do + issue = build(:issue, due_date: Date.tomorrow) + + Timecop.freeze(3.days.from_now) do + expect(issue).to be_overdue + end +end +``` + ### Test speed GitLab has a massive test suite that, without parallelization, can take more @@ -115,6 +134,10 @@ Here are some things to keep in mind regarding test performance: ### Features / Integration +GitLab uses [rspec-rails feature specs] to test features in a browser +environment. These are [capybara] specs running on the headless [poltergeist] +driver. + - Feature specs live in `spec/features/` and should be named `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. - Use only one `feature` block per feature spec file. @@ -122,6 +145,10 @@ Here are some things to keep in mind regarding test performance: - Avoid scenario titles that add no information, such as "successfully." - Avoid scenario titles that repeat the feature title. +[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs +[capybara]: https://github.com/teamcapybara/capybara +[poltergeist]: https://github.com/teampoltergeist/poltergeist + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 205b3e8a835..ead79ba6a10 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the [products]: https://about.gitlab.com/products/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
[android project]: http://source.android.com/
-[creative commons]: http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file +[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png Binary files differnew file mode 100644 index 00000000000..702a8b02262 --- /dev/null +++ b/doc/development/ux_guide/img/harry-robison.png diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png Binary files differnew file mode 100644 index 00000000000..6db257c5b39 --- /dev/null +++ b/doc/development/ux_guide/img/james-mackey.png diff --git a/doc/development/ux_guide/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png Binary files differnew file mode 100644 index 00000000000..2efe1d0b168 --- /dev/null +++ b/doc/development/ux_guide/img/steven-lyons.png diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md index 717a902c424..da410a8de7a 100644 --- a/doc/development/ux_guide/users.md +++ b/doc/development/ux_guide/users.md @@ -1,16 +1,164 @@ -# Users +## UX Personas +* [Nazim Ramesh](#nazim-ramesh) + - Small to medium size organisations using GitLab CE +* [James Mackey](#james-mackey) + - Medium to large size organisations using CE or EE + - Small organisations using EE +* [Karolina Plaskaty](#karolina-plaskaty) + - Using GitLab.com for personal/hobby projects + - Would like to use GitLab at work + - Working for a medium to large size organisation -> TODO: Create personas. Understand the similarities and differences across the below spectrums. +<hr> -## Users by organization +### Nazim Ramesh +- Small to medium size organisations using GitLab CE -- Enterprise -- Medium company -- Small company -- Open source communities +<img src="img/steven-lyons.png" width="300px"> -## Users by role +#### Demographics -- Admin -- Manager -- Developer +- **Age**<br>32 years old +- **Location**<br>Germany +- **Education**<br>Bachelor of Science in Computer Science +- **Occupation**<br>Full-stack web developer +- **Programming experience**<br>Over 10 years +- **Frequently used programming languages**<br>JavaScript, SQL, PHP +- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security. + +#### Motivations +Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab: + +> +“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.” +> + +In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab. + +> +“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.” +> + +Undeterred, Steven decided to migrate a couple of projects across to GitLab. + +> +“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.” +> + +Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab. + +The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab. + +#### Frustrations +##### Adoption to GitLab has been slow +Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits. + +##### Missing Features +Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do. + +##### Regressions and bugs +Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month. + +##### Uses too much RAM and CPU +> +“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHub” +> + +##### UI/UX +GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.” + +#### Goals +* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration. +* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools. +* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc. + +<hr> + +### James Mackey +- Medium to large size organisations using CE or EE +- Small organisations using EE + +<img src="img/james-mackey.png" width="300px"> + +#### Demographics + +- **Age**<br>36 years old +- **Location**<br>US +- **Education**<br>Masters degree in Computer Science +- **Occupation**<br>Full-stack web developer +- **Programming experience**<br>Over 10 years +- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python +- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics. + +#### Motivations +James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and use”. After using GitLab for over a year, he began to wonder about using it at work. James explains: + +> +“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.” +> + +James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub”. + +The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE. + +James feels partially responsible for his organisation’s decision to start using GitLab. + +> +“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.” +> + +#### Frustrations +##### Third Party Integration +Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do. + +##### UX/UI +James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised. + +##### Permissions +> +“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.” +> + +#### Goals +* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed. +* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily. +* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven. + +<hr> + +### Karolina Plaskaty +- Using GitLab.com for personal/hobby projects +- Would like to use GitLab at work +- Working for a medium to large size organisation + +<img src="img/harry-robison.png" width="300px"> + +#### Demographics + +- **Age**<br>26 years old +- **Location**<br>UK +- **Education**<br>Self taught +- **Occupation**<br>Junior web-developer +- **Programming experience**<br>6 years +- **Frequently used programming languages**<br>JavaScript and SQL +- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel. + +#### Motivations +Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” He’s also an avid reader of GitLab’s blog. + +Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms. + +#### Frustrations +##### Unable to use GitLab at work +Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab. + +##### Performance +GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog” which has deterred him from running GitLab on his own machine for just hobby / personal projects. + +##### UX/UI +Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this. + +#### Goals +* To develop his programming experience and to learn from other developers. +* To contribute to both his own and other open source projects. +* To use a fast and intuitive version control platform.
\ No newline at end of file diff --git a/doc/install/installation.md b/doc/install/installation.md index 0f07085942a..5ba338ba7d1 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -39,6 +39,7 @@ The GitLab installation consists of setting up the following components: 1. Packages / Dependencies 1. Ruby 1. Go +1. Node 1. System Users 1. Database 1. Redis @@ -63,7 +64,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake If you want to use Kerberos for user authentication, then install libkrb5-dev: @@ -151,13 +152,30 @@ page](https://golang.org/dl). sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ rm go1.5.3.linux-amd64.tar.gz -## 4. System Users +## 4. Node + +Since GitLab 8.17, GitLab requires the use of node >= v4.3.0 to compile +javascript assets, and starting in GitLab 9.0, yarn >= v0.17.0 is required to +manage javascript dependencies. In many distros the versions provided by the +official package repositories are out of date, so we'll need to install through +the following commands: + + # install node v7.x + curl --location https://deb.nodesource.com/setup_7.x | bash - + sudo apt-get install -y nodejs + + # install yarn + curl --location https://yarnpkg.com/install.sh | bash - + +Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. + +## 5. System Users Create a `git` user for GitLab: sudo adduser --disabled-login --gecos 'GitLab' git -## 5. Database +## 6. Database We recommend using a PostgreSQL database. For MySQL check the [MySQL setup guide](database_mysql.md). @@ -218,7 +236,7 @@ We recommend using a PostgreSQL database. For MySQL check the gitlabhq_production> \q ``` -## 6. Redis +## 7. Redis GitLab requires at least Redis 2.8. @@ -263,7 +281,7 @@ sudo service redis-server restart sudo usermod -aG redis git ``` -## 7. GitLab +## 8. GitLab # We'll install GitLab into home directory of the user "git" cd /home/git @@ -451,7 +469,8 @@ Check if GitLab and its environment are configured correctly: ### Compile Assets - sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production + sudo -u git -H yarn install --production --pure-lockfile + sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production ### Start Your GitLab Instance @@ -459,7 +478,7 @@ Check if GitLab and its environment are configured correctly: # or sudo /etc/init.d/gitlab restart -## 8. Nginx +## 9. Nginx **Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/). diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 30f0c15dacc..242890af981 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,3 +1 @@ -# GitLab LDAP integration - -This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). +This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md). diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md index 073b8797508..4f2b00f3dd1 100644 --- a/doc/profile/preferences.md +++ b/doc/profile/preferences.md @@ -3,13 +3,6 @@ Settings in the **Profile > Preferences** page allow the user to customize various aspects of the site to their liking. -## Application theme - -Changing this setting allows the user to customize the color scheme used for the -navigation bar on the left side of the screen. - -The default is **Charcoal**. - ## Syntax highlighting theme _GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md index 53c2bc560e8..954109ba18f 100644 --- a/doc/update/8.16-to-8.17.md +++ b/doc/update/8.16-to-8.17.md @@ -49,7 +49,19 @@ Install Bundler: sudo gem install bundler --no-ri --no-rdoc ``` -### 4. Get latest code +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +### 5. Get latest code ```bash cd /home/git/gitlab @@ -76,7 +88,7 @@ cd /home/git/gitlab sudo -u git -H git checkout 8-17-stable-ee ``` -### 5. Install libs, migrations, etc. +### 6. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -93,13 +105,16 @@ sudo -u git -H bundle clean # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production +# Install/update frontend asset dependencies +sudo -u git -H npm install --production + # Clean up assets and cache -sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production ``` **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). -### 6. Update gitlab-workhorse +### 7. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl) which should already be on your system from @@ -111,7 +126,7 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ``` -### 7. Update gitlab-shell +### 8. Update gitlab-shell ```bash cd /home/git/gitlab-shell @@ -120,7 +135,7 @@ sudo -u git -H git fetch --all --tags sudo -u git -H git checkout v4.1.1 ``` -### 8. Update configuration files +### 9. Update configuration files #### New configuration options for `gitlab.yml` @@ -194,14 +209,14 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 9. Start application +### 10. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 10. Check application status +### 11. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index f85f4bf8e1e..5ce99843301 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -90,18 +90,43 @@ inside GitLab that make that possible. It is possible to download the latest artifacts of a job via a well known URL so you can use it for scripting purposes. -The structure of the URL is the following: +The structure of the URL to download the whole artifacts archive is the following: ``` https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name> ``` -For example, to download the latest artifacts of the job named `rspec 6 20` of +To download a single file from the artifacts use the following URL: + +``` +https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name> +``` + +For example, to download the latest artifacts of the job named `coverage` of the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org` namespace, the URL would be: ``` -https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20 +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage +``` + +To download the file `coverage/index.html` from the same +artifacts use the following URL: + +``` +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage +``` + +There is also a URL to browse the latest job artifacts: + +``` +https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name> +``` + +For example: + +``` +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage ``` The latest builds are also exposed in the UI in various places. Specifically, diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 80cdb49a1d3..c398ac2eb25 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -62,9 +62,9 @@ pipelines** checkbox and save the changes. ## Badges -In the pipelines settings page you can find job status and test coverage +In the pipelines settings page you can find pipeline status and test coverage badges for your project. The latest successful pipeline will be used to read -the job status and test coverage values. +the pipeline status and test coverage values. Visit the pipelines settings page in your project to see the exact link to your badges, as well as ways to embed the badge image in your HTML or Markdown @@ -72,7 +72,7 @@ pages. ![Pipelines badges](img/pipelines_settings_badges.png) -### Job status badge +### Pipeline status badge Depending on the status of your job, a badge can have the following values: @@ -82,7 +82,7 @@ Depending on the status of your job, a badge can have the following values: - skipped - unknown -You can access a job status badge image using the following link: +You can access a pipeline status badge image using the following link: ``` https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg diff --git a/doc/user/snippets.md b/doc/user/snippets.md new file mode 100644 index 00000000000..417360e08ac --- /dev/null +++ b/doc/user/snippets.md @@ -0,0 +1,19 @@ +# Snippets + +Snippets are little bits of code or text. + +There are 2 types of snippets - project snippets and personal snippets. + +## Project snippets + +Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. + +## Personal snippets + +Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information). + +## Downloading snippets + +You can download the raw content of a snippet. + +By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 7a97b87f1c5..9e7ee47387c 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -39,3 +39,4 @@ - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) +- [Snippets](../user/snippets.md) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index c228ea72f22..4889e3ec50c 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions. ![Master branch and production branch with arrow that indicate deployments](production_branch.png) GitHub flow does assume you are able to deploy to production every time you merge a feature branch. -This is possible for SaaS applications but are many cases where this is not possible. +This is possible for SaaS applications but there are many cases where this is not possible. One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation. Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times. In these cases you can make a production branch that reflects the deployed code. diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 99d7c18f072..6eb457fde3f 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -32,6 +32,29 @@ A Todo appears in your Todos dashboard when: >**Note:** Commenting on a commit will _not_ trigger a Todo. +### Directly addressed Todos + +> [Introduced][ce-7926] in GitLab 9.0. + +If you are mentioned at the start of a line, the todo you receive will be listed +as 'directly addressed'. For instance, in this comment: + +```markdown +@alice What do you think? cc: @bob + +- @carol can you please have a look? + +>>> +@dan what do you think? +>>> + +@erin @frank thank you! +``` + +The people receiving directly addressed todos are `@alice`, `@erin`, and +`@frank`. Directly addressed todos only differ from mention todos in their type, +for filtering; otherwise, they appear as normal. + ### Manually creating a Todo You can also add an issue or merge request to your Todos dashboard by clicking @@ -85,8 +108,9 @@ There are four kinds of filters you can use on your Todos dashboard. | Project | Filter by project | | Author | Filter by the author that triggered the Todo | | Type | Filter by issue or merge request | -| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)| +| Action | Filter by the action that triggered the Todo | You can also filter by more than one of these at the same time. [ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817 +[ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926 diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature deleted file mode 100644 index 99dad88a402..00000000000 --- a/features/dashboard/issues.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Issues - Background: - Given I sign in as a user - And I have authored issues - And I have assigned issues - And I have other issues - And I visit dashboard issues page - - Scenario: I should see assigned issues - Then I should see issues assigned to me - - @javascript - Scenario: I should see authored issues - When I click "Authored by me" link - Then I should see issues authored by me - - @javascript - Scenario: I should see all issues - When I click "All" link - Then I should see all issues diff --git a/features/project/labels.feature b/features/project/labels.feature deleted file mode 100644 index 955bc3d8b1b..00000000000 --- a/features/project/labels.feature +++ /dev/null @@ -1,15 +0,0 @@ -@labels -Feature: Labels - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - When I visit project "Shop" labels page - - @javascript - Scenario: I can subscribe to a label - Then I should see that I am not subscribed to the "bug" label - When I click button "Subscribe" for the "bug" label - Then I should see that I am subscribed to the "bug" label - When I click button "Unsubscribe" for the "bug" label - Then I should see that I am not subscribed to the "bug" label diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature index d767b088883..ec6666f227f 100644 --- a/features/project/merge_requests/revert.feature +++ b/features/project/merge_requests/revert.feature @@ -5,6 +5,7 @@ Feature: Revert Merge Requests And I am signed in as a developer of the project And I am on the Merge Request detail page And I click on Accept Merge Request + And I am on the Merge Request detail page @javascript Scenario: I revert a merge request diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb deleted file mode 100644 index 4e15d79ae74..00000000000 --- a/features/steps/dashboard/issues.rb +++ /dev/null @@ -1,91 +0,0 @@ -class Spinach::Features::DashboardIssues < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include Select2Helper - - step 'I should see issues assigned to me' do - should_see(assigned_issue) - should_not_see(authored_issue) - should_not_see(other_issue) - end - - step 'I should see issues authored by me' do - should_see(authored_issue) - should_see(authored_issue_on_public_project) - should_not_see(assigned_issue) - should_not_see(other_issue) - end - - step 'I should see all issues' do - should_see(authored_issue) - should_see(assigned_issue) - should_see(other_issue) - end - - step 'I have authored issues' do - authored_issue - authored_issue_on_public_project - end - - step 'I have assigned issues' do - assigned_issue - end - - step 'I have other issues' do - other_issue - end - - step 'I click "Authored by me" link' do - find("#assignee_id").set("") - find(".js-author-search", match: :first).click - find(".dropdown-menu-author li a", match: :first, text: current_user.to_reference).click - end - - step 'I click "All" link' do - find(".js-author-search").click - expect(page).to have_selector(".dropdown-menu-author li a") - find(".dropdown-menu-author li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-author li a") - - find(".js-assignee-search").click - expect(page).to have_selector(".dropdown-menu-assignee li a") - find(".dropdown-menu-assignee li a", match: :first).click - expect(page).not_to have_selector(".dropdown-menu-assignee li a") - end - - def should_see(issue) - expect(page).to have_content(issue.title[0..10]) - end - - def should_not_see(issue) - expect(page).not_to have_content(issue.title[0..10]) - end - - def assigned_issue - @assigned_issue ||= create :issue, assignee: current_user, project: project - end - - def authored_issue - @authored_issue ||= create :issue, author: current_user, project: project - end - - def other_issue - @other_issue ||= create :issue, project: project - end - - def authored_issue_on_public_project - @authored_issue_on_public_project ||= create :issue, author: current_user, project: public_project - end - - def project - @project ||= begin - project = create(:empty_project) - project.team << [current_user, :master] - project - end - end - - def public_project - @public_project ||= create(:empty_project, :public) - end -end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 2bbc43b491f..eb906a55a83 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -47,7 +47,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.todos-pending-count') { expect(page).to have_content '3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible) end step 'I mark all todos as done' do @@ -71,7 +71,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done 1' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible) end step 'I should see all todos marked as done' do @@ -81,10 +81,10 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done 4' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false) - should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false) - should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible) + should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible) + should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible) end step 'I filter by "Enterprise"' do @@ -140,15 +140,20 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.should have_css('.identifier', text: 'Merge Request !1') end - def should_see_todo(position, title, body, pending = true) + def should_see_todo(position, title, body, state: :pending) page.within(".todo:nth-child(#{position})") do expect(page).to have_content title expect(page).to have_content body - if pending + if state == :pending expect(page).to have_link 'Done' - else + elsif state == :done_reversible + expect(page).to have_link 'Undo' + elsif state == :done_irreversible + expect(page).not_to have_link 'Undo' expect(page).not_to have_link 'Done' + else + raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible' end end end diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb deleted file mode 100644 index dbeb07c78db..00000000000 --- a/features/steps/project/labels.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spinach::Features::Labels < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedProject - include SharedPaths - - step 'And I visit project "Shop" labels page' do - visit namespace_project_labels_path(project.namespace, project) - end - - step 'I should see that I am subscribed to the "bug" label' do - expect(subscribe_button).to have_content 'Unsubscribe' - end - - step 'I should see that I am not subscribed to the "bug" label' do - expect(subscribe_button).to have_content 'Subscribe' - end - - step 'I click button "Unsubscribe" for the "bug" label' do - subscribe_button.click - end - - step 'I click button "Subscribe" for the "bug" label' do - subscribe_button.click - end - - private - - def subscribe_button - first('.js-subscribe-button', visible: true) - end -end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index d008a8a26af..5bc3a1f5ac4 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -11,7 +11,7 @@ module SharedBuilds step 'project has a recent build' do @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build_with_coverage, pipeline: @pipeline) + @build = create(:ci_build, :coverage, pipeline: @pipeline) end step 'recent build is successful' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 06346ae822a..dbb7271ccbd 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,13 +5,22 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::Boards + mount ::API::V3::Branches mount ::API::V3::DeployKeys mount ::API::V3::Issues + mount ::API::V3::Labels mount ::API::V3::Members + mount ::API::V3::MergeRequestDiffs mount ::API::V3::MergeRequests + mount ::API::V3::ProjectHooks mount ::API::V3::Projects mount ::API::V3::ProjectSnippets + mount ::API::V3::Repositories + mount ::API::V3::SystemHooks + mount ::API::V3::Tags mount ::API::V3::Templates + mount ::API::V3::Users end before { allow_access_with_scope :api } diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 58a4df54bea..2ef327217ea 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -28,8 +28,8 @@ module API end get endpoint do if can_read_awardable? - awards = paginate(awardable.award_emoji) - present awards, with: Entities::AwardEmoji + awards = awardable.award_emoji + present paginate(awards), with: Entities::AwardEmoji else not_found!("Award Emoji") end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 13752eb4947..f4226e5a89d 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,6 +1,7 @@ module API - # Boards API class Boards < Grape::API + include PaginationParams + before { authenticate! } params do @@ -11,9 +12,12 @@ module API detail 'This feature was introduced in 8.13' success Entities::Board end + params do + use :pagination + end get ':id/boards' do authorize!(:read_board, user_project) - present user_project.boards, with: Entities::Board + present paginate(user_project.boards), with: Entities::Board end params do @@ -40,9 +44,12 @@ module API detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List end + params do + use :pagination + end get '/lists' do authorize!(:read_board, user_project) - present board_lists, with: Entities::List + present paginate(board_lists), with: Entities::List end desc 'Get a list of a project board' do diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 9331be1f7de..9d1f5a28ef6 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,8 +1,9 @@ require 'mime/types' module API - # Projects API class Branches < Grape::API + include PaginationParams + before { authenticate! } before { authorize! :download_code, user_project } @@ -13,10 +14,13 @@ module API desc 'Get a project repository branches' do success Entities::RepoBranch end + params do + use :pagination + end get ":id/repository/branches" do - branches = user_project.repository.branches.sort_by(&:name) + branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) - present branches, with: Entities::RepoBranch, project: user_project + present paginate(branches), with: Entities::RepoBranch, project: user_project end desc 'Get a single branch' do diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 173083d0ade..3b314c89c6e 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -16,16 +16,13 @@ module API end params do optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: String, desc: 'Only commits after or in this date will be returned' - optional :until, type: String, desc: 'Only commits before or in this date will be returned' + optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' + optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' optional :page, type: Integer, default: 0, desc: 'The page for pagination' optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' optional :path, type: String, desc: 'The file path' end get ":id/repository/commits" do - # TODO remove the next line for 9.0, use DateTime type in the params block - datetime_attributes! :since, :until - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' offset = params[:page] * params[:per_page] diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 3f5183d46a2..69e85c27a65 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -1,12 +1,17 @@ module API class DeployKeys < Grape::API + include PaginationParams + before { authenticate! } + desc 'Return all deploy keys' + params do + use :pagination + end get "deploy_keys" do authenticated_as_admin! - keys = DeployKey.all - present keys, with: Entities::SSHKey + present paginate(DeployKey.all), with: Entities::SSHKey end params do @@ -18,8 +23,11 @@ module API desc "Get a specific project's deploy keys" do success Entities::SSHKey end + params do + use :pagination + end get ":id/deploy_keys" do - present user_project.deploy_keys, with: Entities::SSHKey + present paginate(user_project.deploy_keys), with: Entities::SSHKey end desc 'Get single deploy key' do @@ -85,20 +93,6 @@ module API end end - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/deploy_keys/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy - - present key.deploy_key, with: Entities::SSHKey - end - desc 'Delete deploy key for a project' do success Key end @@ -107,11 +101,9 @@ module API end delete ":id/deploy_keys/:key_id" do key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end + not_found!('Deploy Key') unless key + + key.destroy end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 232f231ddd2..400ee7c92aa 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -26,7 +26,7 @@ module API expose :last_sign_in_at expose :confirmed_at expose :email - expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at + expose :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project diff --git a/lib/api/files.rb b/lib/api/files.rb index 2ecdd747c8e..6e16ccd2fd8 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,5 +1,4 @@ module API - # Projects API class Files < Grape::API helpers do def commit_params(attrs) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 13896dd91b9..7b6fae866eb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -160,22 +160,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 - # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. - # - # Parameters: - # keys (required) - An array consisting of elements that must be parseable as dates from the params hash - def datetime_attributes!(*keys) - keys.each do |key| - begin - params[key] = Time.xmlschema(params[key]) if params[key].present? - rescue ArgumentError - message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ" - render_api_error!(message, 400) - end - end - end - def filter_by_iid(items, iid) items.where(iid: iid) end diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index 2199eea7e5f..0764b58fb4c 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -2,7 +2,7 @@ module API module Helpers module Pagination def paginate(relation) - relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| + relation.page(params[:page]).per(params[:per_page]).tap do |data| add_pagination_headers(data) end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 90fca20d4fa..2b946bfd349 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -10,17 +10,9 @@ module API args.delete(:id) args[:milestone_title] = args.delete(:milestone) + args[:label_name] = args.delete(:labels) - match_all_labels = args.delete(:match_all_labels) - labels = args.delete(:labels) - args[:label_name] = labels if match_all_labels - - issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations - - # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder - if !match_all_labels && labels.present? - issues = issues.includes(:labels).where('labels.title' => labels.split(',')) - end + issues = IssuesFinder.new(current_user, args).execute issues.reorder(args[:order_by] => args[:sort]) end @@ -77,7 +69,7 @@ module API get ":id/issues" do group = find_group!(params[:id]) - issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) + issues = find_issues(group_id: group.id, state: params[:state] || 'opened') present paginate(issues), with: Entities::Issue, current_user: current_user end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 652786d4e3e..d2955af3f95 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,6 +1,7 @@ module API - # Labels API class Labels < Grape::API + include PaginationParams + before { authenticate! } params do @@ -10,8 +11,11 @@ module API desc 'Get all labels of the project' do success Entities::Label end + params do + use :pagination + end get ':id/labels' do - present available_labels, with: Entities::Label, current_user: current_user, project: user_project + present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index bc3d69f6904..4901a7cfea6 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -1,6 +1,8 @@ module API # MergeRequestDiff API class MergeRequestDiffs < Grape::API + include PaginationParams + before { authenticate! } resource :projects do @@ -12,12 +14,12 @@ module API params do requires :id, type: String, desc: 'The ID of a project' requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + use :pagination end - get ":id/merge_requests/:merge_request_id/versions" do merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff + present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff end desc 'Get a single merge request diff version' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8e09a6f7354..bdd764abfeb 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -119,8 +119,9 @@ module API end get ':id/merge_requests/:merge_request_id/commits' do merge_request = find_merge_request_with_access(params[:merge_request_id]) + commits = ::Kaminari.paginate_array(merge_request.commits) - present merge_request.commits, with: Entities::RepoCommit + present paginate(commits), with: Entities::RepoCommit end desc 'Show the merge request changes' do diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 3c373a84ec5..0b4ed76b35c 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -120,6 +120,28 @@ module API issues = IssuesFinder.new(current_user, finder_params).execute present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project end + + desc 'Get all merge requests for a single project milestone' do + detail 'This feature was introduced in GitLab 9.' + success Entities::MergeRequest + end + params do + requires :milestone_id, type: Integer, desc: 'The ID of a project milestone' + use :pagination + end + get ':id/milestones/:milestone_id/merge_requests' do + authorize! :read_milestone, user_project + + milestone = user_project.milestones.find(params[:milestone_id]) + + finder_params = { + project_id: user_project.id, + milestone_id: milestone.id + } + + merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute + present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + end end end end diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb index 8c1e4381a74..f566eb3ed2b 100644 --- a/lib/api/pagination_params.rb +++ b/lib/api/pagination_params.rb @@ -15,8 +15,8 @@ module API included do helpers do params :pagination do - optional :page, type: Integer, desc: 'Current page number' - optional :per_page, type: Integer, desc: 'Number of items per page' + optional :page, type: Integer, default: 1, desc: 'Current page number' + optional :per_page, type: Integer, default: 20, desc: 'Number of items per page' end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index cb679e6658a..f7a28d7ad10 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -32,9 +32,7 @@ module API use :pagination end get ":id/hooks" do - hooks = paginate user_project.hooks - - present hooks, with: Entities::ProjectHook + present paginate(user_project.hooks), with: Entities::ProjectHook end desc 'Get a project hook' do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 68c2732ec80..366e5679edd 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -266,7 +266,7 @@ module API desc 'Unstar a project' do success Entities::Project end - delete ':id/star' do + post ':id/unstar' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 4ca6646a6f1..bfda6f45b0a 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -2,6 +2,8 @@ require 'mime/types' module API class Repositories < Grape::API + include PaginationParams + before { authorize! :download_code, user_project } params do @@ -24,6 +26,7 @@ module API optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :path, type: String, desc: 'The path of the tree' optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' + use :pagination end get ':id/repository/tree' do ref = params[:ref_name] || user_project.try(:default_branch) || 'master' @@ -33,8 +36,8 @@ module API not_found!('Tree') unless commit tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) - - present tree.sorted_entries, with: Entities::RepoTreeObject + entries = ::Kaminari.paginate_array(tree.sorted_entries) + present paginate(entries), with: Entities::RepoTreeObject end desc 'Get a raw file contents' @@ -100,10 +103,13 @@ module API desc 'Get repository contributors' do success Entities::Contributor end + params do + use :pagination + end get ':id/repository/contributors' do begin - present user_project.repository.contributors, - with: Entities::Contributor + contributors = ::Kaminari.paginate_array(user_project.repository.contributors) + present paginate(contributors), with: Entities::Contributor rescue not_found! end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 4816b5ed1b7..4fbd4096533 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -60,8 +60,9 @@ module API put ':id' do runner = get_runner(params.delete(:id)) authenticate_update_runner!(runner) + update_service = Ci::UpdateRunnerService.new(runner) - if runner.update(declared_params(include_missing: false)) + if update_service.update(declared_params(include_missing: false)) present runner, with: Entities::RunnerDetails, current_user: current_user else render_validation_error!(runner) diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 708ec8cfe70..d038a3fa828 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,6 +1,7 @@ module API - # Hooks API class SystemHooks < Grape::API + include PaginationParams + before do authenticate! authenticated_as_admin! @@ -10,10 +11,11 @@ module API desc 'Get the list of system hooks' do success Entities::Hook end + params do + use :pagination + end get do - hooks = SystemHook.all - - present hooks, with: Entities::Hook + present paginate(SystemHook.all), with: Entities::Hook end desc 'Create a new system hook' do diff --git a/lib/api/tags.rb b/lib/api/tags.rb index b6fd8f569a9..86759ab882f 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -1,6 +1,7 @@ module API - # Git Tags API class Tags < Grape::API + include PaginationParams + before { authorize! :download_code, user_project } params do @@ -10,9 +11,12 @@ module API desc 'Get a project repository tags' do success Entities::RepoTag end + params do + use :pagination + end get ":id/repository/tags" do - present user_project.repository.tags.sort_by(&:name).reverse, - with: Entities::RepoTag, project: user_project + tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse) + present paginate(tags), with: Entities::RepoTag, project: user_project end desc 'Get a single repository tag' do diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 8a2d66efd89..0fc13b35d5b 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -1,5 +1,7 @@ module API class Templates < Grape::API + include PaginationParams + GLOBAL_TEMPLATE_TYPES = { gitignores: { klass: Gitlab::Template::GitignoreTemplate, @@ -51,12 +53,14 @@ module API end params do optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' + use :pagination end get "templates/licenses" do options = { featured: declared(params).popular.present? ? true : nil } - present Licensee::License.all(options), with: ::API::Entities::RepoLicense + licences = ::Kaminari.paginate_array(Licensee::License.all(options)) + present paginate(licences), with: Entities::RepoLicense end desc 'Get the text for a specific license' do @@ -82,8 +86,12 @@ module API detail "This feature was introduced in GitLab #{gitlab_version}." success Entities::TemplatesList end + params do + use :pagination + end get "templates/#{template_type}" do - present klass.all, with: Entities::TemplatesList + templates = ::Kaminari.paginate_array(klass.all) + present paginate(templates), with: Entities::TemplatesList end desc 'Get the text for a specific template present in local filesystem' do diff --git a/lib/api/users.rb b/lib/api/users.rb index 82ac3886ac3..fbc17953691 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -209,6 +209,7 @@ module API end params do requires :id, type: Integer, desc: 'The ID of the user' + use :pagination end get ':id/keys' do authenticated_as_admin! @@ -216,7 +217,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user - present user.keys, with: Entities::SSHKey + present paginate(user.keys), with: Entities::SSHKey end desc 'Delete an existing SSH key from a specified user. Available only for admins.' do @@ -266,13 +267,14 @@ module API end params do requires :id, type: Integer, desc: 'The ID of the user' + use :pagination end get ':id/emails' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user - present user.emails, with: Entities::Email + present paginate(user.emails), with: Entities::Email end desc 'Delete an email address of a specified user. Available only for admins.' do @@ -312,7 +314,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/block' do + post ':id/block' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -328,7 +330,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/unblock' do + post ':id/unblock' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -373,8 +375,11 @@ module API desc "Get the currently authenticated user's SSH keys" do success Entities::SSHKey end + params do + use :pagination + end get "keys" do - present current_user.keys, with: Entities::SSHKey + present paginate(current_user.keys), with: Entities::SSHKey end desc 'Get a single key owned by currently authenticated user' do @@ -423,8 +428,11 @@ module API desc "Get the currently authenticated user's email addresses" do success Entities::Email end + params do + use :pagination + end get "emails" do - present current_user.emails, with: Entities::Email + present paginate(current_user.emails), with: Entities::Email end desc 'Get a single email address owned by the currently authenticated user' do diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb new file mode 100644 index 00000000000..31d708bc2c8 --- /dev/null +++ b/lib/api/v3/boards.rb @@ -0,0 +1,51 @@ +module API + module V3 + class Boards < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success ::API::Entities::Board + end + get ':id/boards' do + authorize!(:read_board, user_project) + present user_project.boards, with: ::API::Entities::Board + end + + params do + requires :board_id, type: Integer, desc: 'The ID of a board' + end + segment ':id/boards/:board_id' do + helpers do + def project_board + board = user_project.boards.first + + if params[:board_id] == board.id + board + else + not_found!('Board') + end + end + + def board_lists + project_board.lists.destroyable + end + end + + desc 'Get the lists of a project board' do + detail 'Does not include `done` list. This feature was introduced in 8.13' + success ::API::Entities::List + end + get '/lists' do + authorize!(:read_board, user_project) + present board_lists, with: ::API::Entities::List + end + end + end + end + end +end diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb new file mode 100644 index 00000000000..733c6b21be5 --- /dev/null +++ b/lib/api/v3/branches.rb @@ -0,0 +1,24 @@ +require 'mime/types' + +module API + module V3 + class Branches < Grape::API + before { authenticate! } + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get a project repository branches' do + success ::API::Entities::RepoBranch + end + get ":id/repository/branches" do + branches = user_project.repository.branches.sort_by(&:name) + + present branches, with: ::API::Entities::RepoBranch, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index 081d45165e8..ba5b6fdbe52 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -16,7 +16,8 @@ module API labels = args.delete(:labels) args[:label_name] = labels if match_all_labels - args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) + # IssuesFinder expects iids + args[:iids] = args.delete(:iid) if args.key?(:iid) issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb new file mode 100644 index 00000000000..5c3261311bf --- /dev/null +++ b/lib/api/v3/labels.rb @@ -0,0 +1,19 @@ +module API + module V3 + class Labels < Grape::API + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get all labels of the project' do + success ::API::Entities::Label + end + get ':id/labels' do + present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb new file mode 100644 index 00000000000..3549ea225ef --- /dev/null +++ b/lib/api/v3/repositories.rb @@ -0,0 +1,55 @@ +require 'mime/types' + +module API + module V3 + class Repositories < Grape::API + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + desc 'Get a project repository tree' do + success ::API::Entities::RepoTreeObject + end + params do + optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' + optional :path, type: String, desc: 'The path of the tree' + optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' + end + get ':id/repository/tree' do + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + path = params[:path] || nil + + commit = user_project.commit(ref) + not_found!('Tree') unless commit + + tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive]) + + present tree.sorted_entries, with: ::API::Entities::RepoTreeObject + end + + desc 'Get repository contributors' do + success ::API::Entities::Contributor + end + get ':id/repository/contributors' do + begin + present user_project.repository.contributors, + with: ::API::Entities::Contributor + rescue + not_found! + end + end + end + end + end +end diff --git a/lib/api/v3/system_hooks.rb b/lib/api/v3/system_hooks.rb new file mode 100644 index 00000000000..391510b9ee0 --- /dev/null +++ b/lib/api/v3/system_hooks.rb @@ -0,0 +1,19 @@ +module API + module V3 + class SystemHooks < Grape::API + before do + authenticate! + authenticated_as_admin! + end + + resource :hooks do + desc 'Get the list of system hooks' do + success ::API::Entities::Hook + end + get do + present SystemHook.all, with: ::API::Entities::Hook + end + end + end + end +end diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb new file mode 100644 index 00000000000..016e3d86932 --- /dev/null +++ b/lib/api/v3/tags.rb @@ -0,0 +1,20 @@ +module API + module V3 + class Tags < Grape::API + before { authorize! :download_code, user_project } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + desc 'Get a project repository tags' do + success ::API::Entities::RepoTag + end + get ":id/repository/tags" do + tags = user_project.repository.tags.sort_by(&:name).reverse + present tags, with: ::API::Entities::RepoTag, project: user_project + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb new file mode 100644 index 00000000000..e05e457a5df --- /dev/null +++ b/lib/api/v3/users.rb @@ -0,0 +1,96 @@ +module API + module V3 + class Users < Grape::API + include PaginationParams + + before do + allow_access_with_scope :read_user if request.get? + authenticate! + end + + resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do + desc 'Get the SSH keys of a specified user. Available only for admins.' do + success ::API::Entities::SSHKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/keys' do + authenticated_as_admin! + + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + present paginate(user.keys), with: ::API::Entities::SSHKey + end + + desc 'Get the emails addresses of a specified user. Available only for admins.' do + success ::API::Entities::Email + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + use :pagination + end + get ':id/emails' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + present user.emails, with: ::API::Entities::Email + end + + desc 'Block a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/block' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if !user.ldap_blocked? + user.block + else + forbidden!('LDAP blocked users cannot be modified by the API') + end + end + + desc 'Unblock a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/unblock' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if user.ldap_blocked? + forbidden!('LDAP blocked users cannot be unblocked by the API') + else + user.activate + end + end + end + + resource :user do + desc "Get the currently authenticated user's SSH keys" do + success ::API::Entities::SSHKey + end + params do + use :pagination + end + get "keys" do + present current_user.keys, with: ::API::Entities::SSHKey + end + + desc "Get the currently authenticated user's email addresses" do + success ::API::Entities::Email + end + get "emails" do + present current_user.emails, with: ::API::Entities::Email + end + end + end + end +end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 955d857c679..3b15ff6566f 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -33,7 +33,12 @@ module Banzai # Returns a String replaced with the return of the block. def self.references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match| - yield match, $~[object_sym].to_i, $~[:project], $~[:namespace], $~ + symbol = $~[object_sym] + if object_class.reference_valid?(symbol) + yield match, symbol.to_i, $~[:project], $~[:namespace], $~ + else + match + end end end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index 0d31660039a..3674ba25641 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -10,7 +10,7 @@ module Gitlab private - def new_issue + def new_issue { attachments: [ { @@ -38,7 +38,7 @@ module Gitlab end def project_link - "[#{project.name_with_namespace}](#{projects_url(project)})" + "[#{project.name_with_namespace}](#{project.web_url})" end def author_profile_link diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index d1bc2055ba8..1e52b6614a1 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -13,6 +13,10 @@ module Gitlab :code end + def legend + "Related Merge Requests" + end + def description "Time until first merge request" end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index d2068fbc38f..213994988a5 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -14,6 +14,10 @@ module Gitlab :issue end + def legend + "Related Issues" + end + def description "Time before an issue gets scheduled" end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 3b4dfc6a30e..45d51d30ccc 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -14,6 +14,10 @@ module Gitlab :plan end + def legend + "Related Commits" + end + def description "Time before an issue starts implementation" end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb index 2a6bcc80116..9f387a02945 100644 --- a/lib/gitlab/cycle_analytics/production_stage.rb +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -15,6 +15,10 @@ module Gitlab :production end + def legend + "Related Issues" + end + def description "From issue creation until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index fbaa3010d81..4744be834de 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -13,6 +13,10 @@ module Gitlab :review end + def legend + "Relative Merged Requests" + end + def description "Time between merge request creation and merge/close" end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index 945909a4d62..3cdbe04fbaf 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -14,6 +14,10 @@ module Gitlab :staging end + def legend + "Relative Deployed Builds" + end + def description "From merge request merge until deploy to production" end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index 0079d56e0e4..e96943833bc 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -13,6 +13,10 @@ module Gitlab :test end + def legend + "Relative Builds Trigger by Commits" + end + def description "Total test time for all commits/merges" end diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 6548e6475c6..f78106f5b10 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -8,6 +8,8 @@ module Gitlab commit = build.pipeline user = build.user + author_url = build_author_url(build.commit, commit) + data = { object_kind: 'build', @@ -43,6 +45,7 @@ module Gitlab message: commit.git_commit_message, author_name: commit.git_author_name, author_email: commit.git_author_email, + author_url: author_url, status: commit.status, duration: commit.duration, started_at: commit.started_at, @@ -62,6 +65,13 @@ module Gitlab data end + + private + + def build_author_url(commit, pipeline) + author = commit.try(:author) + author ? Gitlab::Routing.url_helpers.user_url(author) : "mailto:#{pipeline.git_author_email}" + end end end end diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 95dba9a327b..8c80791e7c9 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -1,11 +1,12 @@ module Gitlab module GithubImport class BaseFormatter - attr_reader :formatter, :project, :raw_data + attr_reader :client, :formatter, :project, :raw_data - def initialize(project, raw_data) + def initialize(project, raw_data, client = nil) @project = project @raw_data = raw_data + @client = client @formatter = Gitlab::ImportFormatter.new end @@ -18,19 +19,6 @@ module Gitlab def url raw_data.url || '' end - - private - - def gitlab_user_id(github_id) - User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). - try(:id) - end - - def gitlab_author_id - return @gitlab_author_id if defined?(@gitlab_author_id) - @gitlab_author_id = gitlab_user_id(raw_data.user.id) - end end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index ba869faa92e..7dbeec5b010 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -10,6 +10,7 @@ module Gitlab @access_token = access_token @host = host.to_s.sub(%r{/+\z}, '') @api_version = api_version + @users = {} if access_token ::Octokit.auto_paginate = false @@ -64,6 +65,13 @@ module Gitlab api.respond_to?(method) || super end + def user(login) + return nil unless login.present? + return @users[login] if @users.key?(login) + + @users[login] = api.user(login) + end + private def api_endpoint diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 2bddcde2b7c..e21922070c1 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -1,6 +1,8 @@ module Gitlab module GithubImport class CommentFormatter < BaseFormatter + attr_writer :author_id + def attributes { project: project, @@ -17,11 +19,11 @@ module Gitlab private def author - raw_data.user.login + @author ||= UserFormatter.new(client, raw_data.user) end def author_id - gitlab_author_id || project.creator_id + author.gitlab_id || project.creator_id end def body @@ -52,10 +54,10 @@ module Gitlab end def note - if gitlab_author_id + if author.gitlab_id body else - formatter.author_line(author) + body + formatter.author_line(author.login) + body end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9a4ffd28438..d95ff4fd104 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -110,7 +110,7 @@ module Gitlab def import_issues fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| - gh_issue = IssueFormatter.new(project, raw) + gh_issue = IssueFormatter.new(project, raw, client) begin issuable = @@ -131,7 +131,8 @@ module Gitlab def import_pull_requests fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| - gh_pull_request = PullRequestFormatter.new(project, raw) + gh_pull_request = PullRequestFormatter.new(project, raw, client) + next unless gh_pull_request.valid? begin @@ -209,14 +210,16 @@ module Gitlab ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) + comment = CommentFormatter.new(project, raw, client) + # GH does not return info about comment's parent, so we guess it by checking its URL! *_, parent, iid = URI(raw.html_url).path.split('/') - if parent == 'issues' - issuable = Issue.find_by(project_id: project.id, iid: iid) - else - issuable = MergeRequest.find_by(target_project_id: project.id, iid: iid) - end + + issuable = if parent == 'issues' + Issue.find_by(project_id: project.id, iid: iid) + else + MergeRequest.find_by(target_project_id: project.id, iid: iid) + end next unless issuable diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb index 256f360efc7..29fb0f9d333 100644 --- a/lib/gitlab/github_import/issuable_formatter.rb +++ b/lib/gitlab/github_import/issuable_formatter.rb @@ -1,6 +1,8 @@ module Gitlab module GithubImport class IssuableFormatter < BaseFormatter + attr_writer :assignee_id, :author_id + def project_association raise NotImplementedError end @@ -23,18 +25,24 @@ module Gitlab raw_data.assignee.present? end - def assignee_id + def author + @author ||= UserFormatter.new(client, raw_data.user) + end + + def author_id + @author_id ||= author.gitlab_id || project.creator_id + end + + def assignee if assigned? - gitlab_user_id(raw_data.assignee.id) + @assignee ||= UserFormatter.new(client, raw_data.assignee) end end - def author - raw_data.user.login - end + def assignee_id + return @assignee_id if defined?(@assignee_id) - def author_id - gitlab_author_id || project.creator_id + @assignee_id = assignee.try(:gitlab_id) end def body @@ -42,10 +50,10 @@ module Gitlab end def description - if gitlab_author_id + if author.gitlab_id body else - formatter.author_line(author) + body + formatter.author_line(author.login) + body end end diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb new file mode 100644 index 00000000000..04c2964da20 --- /dev/null +++ b/lib/gitlab/github_import/user_formatter.rb @@ -0,0 +1,45 @@ +module Gitlab + module GithubImport + class UserFormatter + attr_reader :client, :raw + + delegate :id, :login, to: :raw, allow_nil: true + + def initialize(client, raw) + @client = client + @raw = raw + end + + def gitlab_id + return @gitlab_id if defined?(@gitlab_id) + + @gitlab_id = find_by_external_uid || find_by_email + end + + private + + def email + @email ||= client.user(raw.login).try(:email) + end + + def find_by_email + return nil unless email + + User.find_by_any_email(email) + .try(:id) + end + + def find_by_external_uid + return nil unless id + + identities = ::Identity.arel_table + + User.select(:id) + .joins(:identities).where(identities[:provider].eq(:github) + .and(identities[:extern_uid].eq(id))) + .first + .try(:id) + end + end + end +end diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb index a672e5e4855..6dbb467d70d 100644 --- a/lib/gitlab/slash_commands/extractor.rb +++ b/lib/gitlab/slash_commands/extractor.rb @@ -103,7 +103,7 @@ module Gitlab (?<cmd>#{Regexp.union(names)}) (?: [ ] - (?<arg>[^\/\n]*) + (?<arg>[^\n]*) )? (?:\n|$) ) diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb deleted file mode 100644 index 19ab76ae80f..00000000000 --- a/lib/gitlab/themes.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Gitlab - # Module containing GitLab's application theme definitions and helper methods - # for accessing them. - module Themes - extend self - - # Theme ID used when no `default_theme` configuration setting is provided. - APPLICATION_DEFAULT = 2 - - # Struct class representing a single Theme - Theme = Struct.new(:id, :name, :css_class) - - # All available Themes - THEMES = [ - Theme.new(1, 'Graphite', 'ui_graphite'), - Theme.new(2, 'Charcoal', 'ui_charcoal'), - Theme.new(3, 'Green', 'ui_green'), - Theme.new(4, 'Black', 'ui_black'), - Theme.new(5, 'Violet', 'ui_violet'), - Theme.new(6, 'Blue', 'ui_blue') - ].freeze - - # Convenience method to get a space-separated String of all the theme - # classes that might be applied to the `body` element - # - # Returns a String - def body_classes - THEMES.collect(&:css_class).uniq.join(' ') - end - - # Get a Theme by its ID - # - # If the ID is invalid, returns the default Theme. - # - # id - Integer ID - # - # Returns a Theme - def by_id(id) - THEMES.detect { |t| t.id == id } || default - end - - # Returns the number of defined Themes - def count - THEMES.size - end - - # Get the default Theme - # - # Returns a Theme - def default - by_id(default_id) - end - - # Iterate through each Theme - # - # Yields the Theme object - def each(&block) - THEMES.each(&block) - end - - # Get the Theme for the specified user, or the default - # - # user - User record - # - # Returns a Theme - def for_user(user) - if user - by_id(user.theme_id) - else - default - end - end - - private - - def default_id - id = Gitlab.config.gitlab.default_theme.to_i - - # Prevent an invalid configuration setting from causing an infinite loop - if id < THEMES.first.id || id > THEMES.last.id - APPLICATION_DEFAULT - else - id - end - end - end -end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index e78d0c34a02..4cc34e34460 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -61,13 +61,16 @@ module Gitlab "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}), "Install gems" => %W(bundle), "Migrate DB" => %W(bundle exec rake db:migrate), - "Recompile assets" => %W(bundle exec rake gitlab:assets:clean gitlab:assets:compile), + "Recompile assets" => %W(bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile), "Clear cache" => %W(bundle exec rake cache:clear) } end def env - { 'RAILS_ENV' => 'production' } + { + 'RAILS_ENV' => 'production', + 'NODE_ENV' => 'production' + } end def upgrade diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake index 2514b050695..51f5d768102 100644 --- a/lib/tasks/eslint.rake +++ b/lib/tasks/eslint.rake @@ -1,7 +1,8 @@ unless Rails.env.production? desc "GitLab | Run ESLint" - task :eslint do - system("yarn", "run", "eslint") + task eslint: ['yarn:check'] do + unless system('yarn run eslint') + abort('rake eslint failed') + end end end - diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index b6ef8260191..3eb5fc07b3c 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -1,21 +1,21 @@ namespace :gitlab do namespace :assets 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 + task compile: [ + 'yarn:check', + 'assets:precompile', + 'webpack:compile', + 'gitlab:assets:fix_urls' + ] desc 'GitLab | Assets | Clean up old compiled frontend assets' - task :clean do - Rake::Task['assets:clean'].invoke - end + task clean: ['assets:clean'] desc 'GitLab | Assets | Remove all compiled frontend assets' - task :purge do - Rake::Task['assets:clobber'].invoke - end + task purge: ['assets:clobber'] + + desc 'GitLab | Assets | Uninstall frontend dependencies' + task purge_modules: ['yarn:clobber'] desc 'GitLab | Assets | Fix all absolute url references in CSS' task :fix_urls do diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake index 35cfed9dc75..40465ea3bf0 100644 --- a/lib/tasks/karma.rake +++ b/lib/tasks/karma.rake @@ -1,6 +1,4 @@ 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| @@ -10,7 +8,7 @@ unless Rails.env.production? end desc 'GitLab | Karma | Run JavaScript tests' - task :tests do + task tests: ['yarn:check'] do sh "yarn run karma" do |ok, res| abort('rake karma:tests failed') unless ok end @@ -18,8 +16,5 @@ unless Rails.env.production? 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 + task karma: ['karma:fixtures', 'karma:tests'] end diff --git a/lib/tasks/yarn.rake b/lib/tasks/yarn.rake new file mode 100644 index 00000000000..2ac88a039e7 --- /dev/null +++ b/lib/tasks/yarn.rake @@ -0,0 +1,40 @@ + +namespace :yarn do + desc 'Ensure Yarn is installed' + task :available do + unless system('yarn --version', out: File::NULL) + warn( + 'Error: Yarn executable was not detected in the system.'.color(:red), + 'Download Yarn at https://yarnpkg.com/en/docs/install'.color(:green) + ) + abort + end + end + + desc 'Ensure Node dependencies are installed' + task check: ['yarn:available'] do + unless system('yarn check --ignore-engines', out: File::NULL) + warn( + 'Error: You have unmet dependencies. (`yarn check` command failed)'.color(:red), + 'Run `yarn install` to install missing modules.'.color(:green) + ) + abort + end + end + + desc 'Install Node dependencies with Yarn' + task install: ['yarn:available'] do + unless system('yarn install --pure-lockfile --ignore-engines') + abort 'Error: Unable to install node modules.'.color(:red) + end + end + + desc 'Remove Node dependencies' + task :clobber do + warn 'Purging ./node_modules directory'.color(:red) + FileUtils.rm_rf 'node_modules' + end +end + +desc 'Install Node dependencies with Yarn' +task yarn: ['yarn:install'] diff --git a/package.json b/package.json index 08bde1bc313..ad0aaef1897 100644 --- a/package.json +++ b/package.json @@ -15,27 +15,24 @@ "babel-loader": "^6.2.10", "babel-preset-es2015": "^6.22.0", "babel-preset-stage-2": "^6.22.0", - "bootstrap-sass": "3.3.6", + "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.2", - "d3": "3.5.11", - "dropzone": "4.2.0", + "d3": "^3.5.11", + "dropzone": "^4.2.0", "es6-promise": "^4.0.5", - "imports-loader": "^0.6.5", - "jquery": "2.2.1", - "jquery-ui": "github:jquery/jquery-ui#1.11.4", - "jquery-ujs": "1.2.1", + "jquery": "^2.2.1", + "jquery-ui": "git+https://github.com/jquery/jquery-ui#1.11.4", + "jquery-ujs": "^1.2.1", "js-cookie": "^2.1.3", - "karma-mocha-reporter": "^2.2.2", - "mousetrap": "1.4.6", + "mousetrap": "^1.4.6", "pikaday": "^1.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", "timeago.js": "^2.0.5", - "underscore": "1.8.3", - "vue": "2.0.3", - "vue-resource": "0.9.3", - "webpack": "^2.2.1", - "webpack-dev-server": "^2.3.0" + "underscore": "^1.8.3", + "vue": "^2.0.3", + "vue-resource": "^0.9.3", + "webpack": "^2.2.1" }, "devDependencies": { "babel-plugin-istanbul": "^4.0.0", @@ -51,9 +48,11 @@ "karma": "^1.4.1", "karma-coverage-istanbul-reporter": "^0.2.0", "karma-jasmine": "^1.1.0", + "karma-mocha-reporter": "^2.2.2", "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.2" + "karma-webpack": "^2.0.2", + "webpack-dev-server": "^2.3.0" }, "nyc": { "exclude": [ diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb new file mode 100644 index 00000000000..b5fe40d0510 --- /dev/null +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Admin::RunnersController do + let(:runner) { create(:ci_runner) } + + before do + sign_in(create(:admin)) + end + + describe '#index' do + it 'lists all runners' do + get :index + + expect(response).to have_http_status(200) + end + end + + describe '#show' do + it 'shows a particular runner' do + get :show, id: runner.id + + expect(response).to have_http_status(200) + end + + it 'shows 404 for unknown runner' do + get :show, id: 0 + + expect(response).to have_http_status(404) + end + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, id: runner.id, runner: { description: new_desc } + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, id: runner.id + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, id: runner.id + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 79ef3a1adad..0a3ac9f9512 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -1,16 +1,19 @@ require 'spec_helper' describe Dashboard::TodosController do + include ApiHelpers + let(:user) { create(:user) } + let(:author) { create(:user) } let(:project) { create(:empty_project) } let(:todo_service) { TodoService.new } - describe 'GET #index' do - before do - sign_in(user) - project.team << [user, :developer] - end + before do + sign_in(user) + project.team << [user, :developer] + end + describe 'GET #index' do context 'when using pagination' do let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } @@ -34,4 +37,16 @@ describe Dashboard::TodosController do end end end + + describe 'PATCH #restore' do + let(:todo) { create(:todo, :done, user: user, project: project, author: author) } + + it 'restores the todo to pending state' do + patch :restore, id: todo.id + + expect(todo.reload).to be_pending + expect(response).to have_http_status(200) + expect(json_response).to eq({ "count" => 1, "done_count" => 0 }) + end + end end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 8f02003992a..7b3aa0491c7 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,8 +25,7 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars', - theme_id: '1' + dashboard: 'stars' ) patch :update, user: params, format: format @@ -41,8 +40,7 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars', - theme_id: '2' + dashboard: 'stars' }.with_indifferent_access expect(user).to receive(:update_attributes).with(prefs) diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b36d0e69330..7d4636e98d1 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -86,32 +86,47 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:guest) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, guest).execute } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") } - - before { sign_in(guest) } - - it "redirects to forked project new merge request" do - default_params[:target_branch] = "fork-test-1" - default_params[:create_merge_request] = 1 - - allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success) - - put :update, default_params - - expect(response).to redirect_to( - new_namespace_project_merge_request_path( - forked_project.namespace, - forked_project, - merge_request: { - source_project_id: forked_project.id, - target_project_id: project.id, - source_branch: "fork-test-1", - target_branch: "master" - } + let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } + let!(:forked_project) { forked_project_link.forked_to_project } + let(:guest) { forked_project.owner } + + before do + sign_in(guest) + end + + context 'when editing on the fork' do + before do + default_params[:namespace_id] = forked_project.namespace.to_param + default_params[:project_id] = forked_project.to_param + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG')) + end + end + + context 'when editing on the original repository' do + it "redirects to forked project new merge request" do + default_params[:target_branch] = "fork-test-1" + default_params[:create_merge_request] = 1 + + put :update, default_params + + expect(response).to redirect_to( + new_namespace_project_merge_request_path( + forked_project.namespace, + forked_project, + merge_request: { + source_project_id: forked_project.id, + target_project_id: project.id, + source_branch: "fork-test-1", + target_branch: "master" + } + ) ) - ) + end end end end diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb new file mode 100644 index 00000000000..0fa249e4405 --- /dev/null +++ b/spec/controllers/projects/runners_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Projects::RunnersController do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:runner) { create(:ci_runner) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + id: runner + } + end + + before do + sign_in(user) + project.add_master(user) + project.runners << runner + end + + describe '#update' do + it 'updates the runner and ticks the queue' do + new_desc = runner.description.swapcase + + expect do + post :update, params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.description).to eq(new_desc) + end + end + + describe '#destroy' do + it 'destroys the runner' do + delete :destroy, params + + expect(response).to have_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + describe '#resume' do + it 'marks the runner as active and ticks the queue' do + runner.update(active: false) + + expect do + post :resume, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(true) + end + end + + describe '#pause' do + it 'marks the runner as inactive and ticks the queue' do + runner.update(active: true) + + expect do + post :pause, params + end.to change { runner.ensure_runner_queue_value } + + runner.reload + + expect(response).to have_http_status(302) + expect(runner.active).to eq(false) + end + end +end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 19e948d8fb8..77ee10a1e15 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -206,4 +206,37 @@ describe Projects::SnippetsController do end end end + + describe 'GET #raw' do + let(:project_snippet) do + create( + :project_snippet, :public, + project: project, + author: user, + content: "first line\r\nsecond line\r\nthird line" + ) + end + + context 'CRLF line ending' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: project_snippet.to_param + } + end + + it 'returns LF line endings by default' do + get :raw, params + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get :raw, params.merge(line_ending: :raw) + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end + end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index dadcb90cfc2..f90c0d76ceb 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -286,6 +286,24 @@ describe SnippetsController do expect(assigns(:snippet)).to eq(personal_snippet) expect(response).to have_http_status(200) end + + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end + + it 'returns LF line endings by default' do + get action, id: personal_snippet.to_param + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get action, id: personal_snippet.to_param, line_ending: :raw + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end end context 'when not signed in' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0397d5d4001..a90534d10ba 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -89,8 +89,9 @@ FactoryGirl.define do tag true end - factory :ci_build_with_coverage do + trait :coverage do coverage 99.9 + coverage_regex '/(d+)/' end trait :trace do @@ -99,6 +100,16 @@ FactoryGirl.define do end end + trait :erased do + erased_at Time.now + erased_by factory: :user + end + + trait :queued do + queued_at Time.now + runner factory: :ci_runner + end + trait :artifacts do after(:create) do |build, _| build.artifacts_file = @@ -128,5 +139,17 @@ FactoryGirl.define do build.save! end end + + trait :with_commit do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit, :without_author) + end + end + + trait :with_commit_and_author do + after(:build) do |build| + allow(build).to receive(:commit).and_return build(:commit) + end + end end end diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index ac6eb0a7897..89e260cf65b 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -8,5 +8,15 @@ FactoryGirl.define do initialize_with do new(git_commit, project) end + + after(:build) do |commit| + allow(commit).to receive(:author).and_return build(:author) + end + + trait :without_author do + after(:build) do |commit| + allow(commit).to receive(:author).and_return nil + end + end end end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index b4e4cd97780..a5265f1b189 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -40,6 +40,10 @@ FactoryGirl.define do action { Todo::UNMERGEABLE } end + trait :pending do + state :pending + end + trait :done do state :done end diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 7fcfe5a54c7..340884fc986 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -30,5 +30,24 @@ describe "Admin::AbuseReports", feature: true, js: true do end end end + + describe 'if a many users have been reported for abuse' do + let(:report_count) { AbuseReport.default_per_page + 3 } + + before do + report_count.times do + create(:abuse_report, user: create(:user)) + end + end + + describe 'in the abuse report view' do + it 'presents information about abuse report' do + visit admin_abuse_reports_path + + expect(page).to have_selector('.pagination') + expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) + end + end + end end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index f05fbe3d062..5dcc7d35d82 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -18,7 +18,7 @@ describe "Admin Runners" do it 'has all necessary texts' do expect(page).to have_text "To register a new Runner" - expect(page).to have_text "Runners with last contact less than a minute ago: 1" + expect(page).to have_text "Runners with last contact more than a minute ago: 1" end describe 'search' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 7225f38b7e5..1b25b51cfb2 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Welcome to your Issue Board!') end + it 'disables add issues button by default' do + button = page.find('.issue-boards-search button', text: 'Add issues') + + expect(button[:disabled]).to eq true + end + it 'hides the blank state when clicking nevermind button' do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 7651364703e..59e87b3f69c 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -15,8 +15,11 @@ describe 'Issue Boards', feature: true, js: true do 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) } + let(:card) { first('.board').first('.card') } before do + Timecop.freeze + project.team << [user, :master] login_as(user) @@ -25,32 +28,28 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource end + after do + Timecop.return + end + it 'shows sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking issue' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).not_to have_selector('.issue-boards-sidebar') end it 'closes sidebar when clicking close button' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) expect(page).to have_selector('.issue-boards-sidebar') @@ -60,9 +59,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'shows issue details when sidebar is open' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.issue-boards-sidebar') do expect(page).to have_content(issue2.title) @@ -70,15 +67,15 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'removes card from board when clicking remove button' do - page.within(first('.board')) do - first('.card').click - end + it 'removes card from board when clicking ' do + click_card(card) page.within('.issue-boards-sidebar') do click_button 'Remove from board' end + wait_for_vue_resource + page.within(first('.board')) do expect(page).to have_selector('.card', count: 1) end @@ -86,9 +83,7 @@ describe 'Issue Boards', feature: true, js: true do context 'assignee' do it 'updates the issues assignee' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -104,17 +99,12 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'removes the assignee' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + card_two = first('.board').find('.card:nth-child(2)') + click_card(card_two) page.within('.assignee') do click_link 'Edit' @@ -130,17 +120,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('No assignee') end - page.within(first('.board')) do - page.within(find('.card:nth-child(2)')) do - expect(page).not_to have_selector('.avatar') - end - end + expect(card_two).not_to have_selector('.avatar') end it 'assignees to current user' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within(find('.assignee')) do expect(page).to have_content('No assignee') @@ -152,17 +136,11 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.avatar') - end - end + expect(card).to have_selector('.avatar') end it 'resets assignee dropdown' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.assignee') do click_link 'Edit' @@ -192,9 +170,7 @@ describe 'Issue Boards', feature: true, js: true do context 'milestone' do it 'adds a milestone' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -212,9 +188,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes a milestone' do - page.within(first('.board')) do - find('.card:nth-child(2)').click - end + click_card(card) page.within('.milestone') do click_link 'Edit' @@ -234,9 +208,7 @@ describe 'Issue Boards', feature: true, js: true do context 'due date' do it 'updates due date' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.due_date') do click_link 'Edit' @@ -252,9 +224,7 @@ describe 'Issue Boards', feature: true, js: true do context 'labels' do it 'adds a single label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -273,18 +243,12 @@ describe 'Issue Boards', feature: true, js: true do 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(bug.title) - end - end + expect(card).to have_selector('.label', count: 2) + expect(card).to have_content(bug.title) end it 'adds a multiple labels' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -305,19 +269,13 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).to have_selector('.label', count: 3) - expect(page).to have_content(bug.title) - expect(page).to have_content(regression.title) - end - end + expect(card).to have_selector('.label', count: 3) + expect(card).to have_content(bug.title) + expect(card).to have_content(regression.title) end it 'removes a label' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.labels') do click_link 'Edit' @@ -336,20 +294,14 @@ describe 'Issue Boards', feature: true, js: true do end end - page.within(first('.board')) do - page.within(first('.card')) do - expect(page).not_to have_selector('.label') - expect(page).not_to have_content(stretch.title) - end - end + expect(card).not_to have_selector('.label') + expect(card).not_to have_content(stretch.title) end end context 'subscription' do it 'changes issue subscription' do - page.within(first('.board')) do - first('.card').click - end + click_card(card) page.within('.subscription') do click_button 'Subscribe' @@ -358,4 +310,19 @@ describe 'Issue Boards', feature: true, js: true do end end end + + def click_card(card) + page.within(card) do + first('.card-number').click + end + + wait_for_sidebar + end + + def wait_for_sidebar + # loop until the CSS transition is complete + Timeout.timeout(0.5) do + loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 + end + end end diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index 7d59fcac517..ae750be4d4a 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' -RSpec.describe 'Dashboard Active Tab', feature: true do +RSpec.describe 'Dashboard Active Tab', js: true, feature: true do before do login_as :user end shared_examples 'page has active tab' do |title| it "#{title} tab" do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - expect(find('.nav-sidebar li.active')).to have_content(title) + find('.global-dropdown-toggle').trigger('click') + expect(page).to have_selector('.global-dropdown-menu li.active', count: 1) + expect(find('.global-dropdown-menu li.active')).to have_content(title) end end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index 41dcfe439c2..a1718912fc6 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -35,8 +35,9 @@ describe 'Navigation bar counter', feature: true, js: true, caching: true do end def expect_counters(issuable_type, count) - dashboard_count = find('li.active span.badge') - nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count") + dashboard_count = find('li.active') + find('.global-dropdown-toggle').click + nav_count = find(".dashboard-shortcuts-#{issuable_type}") expect(nav_count).to have_content(count) expect(dashboard_count).to have_content(count) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb new file mode 100644 index 00000000000..2db1cf71209 --- /dev/null +++ b/spec/features/dashboard/issues_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Issues', feature: true do + let(:current_user) { create :user } + let(:public_project) { create(:empty_project, :public) } + let(:project) do + create(:empty_project) do |project| + project.team << [current_user, :master] + end + end + + let!(:authored_issue) { create :issue, author: current_user, project: project } + let!(:authored_issue_on_public_project) { create :issue, author: current_user, project: public_project } + let!(:assigned_issue) { create :issue, assignee: current_user, project: project } + let!(:other_issue) { create :issue, project: project } + + before do + login_as(current_user) + + visit issues_dashboard_path(assignee_id: current_user.id) + end + + it 'shows issues assigned to current user' do + expect(page).to have_content(assigned_issue.title) + expect(page).not_to have_content(authored_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows issues when current user is author', js: true do + find('#assignee_id', visible: false).set('') + find('.js-author-search', match: :first).click + find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).not_to have_content(assigned_issue.title) + expect(page).not_to have_content(other_issue.title) + end + + it 'shows all issues' do + click_link('Reset filters') + + expect(page).to have_content(authored_issue.title) + expect(page).to have_content(authored_issue_on_public_project.title) + expect(page).to have_content(assigned_issue.title) + expect(page).to have_content(other_issue.title) + end +end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index d9be4e5dbdd..62a2c54c94c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -10,20 +10,20 @@ feature 'Dashboard shortcuts', feature: true, js: true do find('body').native.send_key('g') find('body').native.send_key('p') - ensure_active_main_tab('Projects') + check_page_title('Projects') find('body').native.send_key('g') find('body').native.send_key('i') - ensure_active_main_tab('Issues') + check_page_title('Issues') find('body').native.send_key('g') find('body').native.send_key('m') - ensure_active_main_tab('Merge Requests') + check_page_title('Merge Requests') end - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) + def check_page_title(title) + expect(find('.header-content .title')).to have_content(title) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 93139dc9e94..7135565294b 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -182,6 +182,20 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + it 'triggers autocomplete after selecting a slash command' do + note = find('#note_note') + page.within '.timeline-content-form' do + note.native.send_keys('') + note.native.send_keys('/as') + note.click + end + + find('.atwho-view li', text: '/assign').native.send_keys(:tab) + + user_item = find('.atwho-view li', text: user.username) + expect(user_item).to have_content(user.username) + end + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 755162a1eb5..094f645a077 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -382,7 +382,9 @@ describe 'Issues', feature: true do it 'changes incoming email address token', js: true do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').click + find('.incoming-email-token-reset').trigger('click') + + wait_for_ajax expect(page).to have_no_field('issue_email', with: previous_token) new_token = project1.new_issue_address(@user.reload) @@ -636,7 +638,7 @@ describe 'Issues', feature: true do it 'removes due date from issue' do date = Date.today.at_beginning_of_month + 2.days - + page.within '.due_date' do click_link 'Edit' diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index a6b841c0210..15c8677fcd3 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -8,35 +8,6 @@ describe 'Profile > Preferences', feature: true do visit profile_preferences_path end - describe 'User changes their application theme', js: true do - let(:default) { Gitlab::Themes.default } - let(:theme) { Gitlab::Themes.by_id(5) } - - it 'creates a flash message' do - choose "user_theme_id_#{theme.id}" - - expect_preferences_saved_message - end - - it 'updates their preference' do - choose "user_theme_id_#{theme.id}" - - allowing_for_delay do - visit page.current_path - expect(page).to have_checked_field("user_theme_id_#{theme.id}") - end - end - - it 'reflects the changes immediately' do - expect(page).to have_selector("body.#{default.css_class}") - - choose "user_theme_id_#{theme.id}" - - expect(page).not_to have_selector("body.#{default.css_class}") - expect(page).to have_selector("body.#{theme.css_class}") - end - end - describe 'User changes their syntax highlighting theme', js: true do it 'creates a flash message' do choose 'user_color_scheme_id_5' diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index b56be499264..8d1214dedb4 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -285,6 +285,27 @@ describe 'Pipelines', :feature, :js do end end end + + context 'with pagination' do + before do + allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) + create(:ci_empty_pipeline, project: project) + end + + it 'should render pagination' do + visit namespace_project_pipelines_path(project.namespace, project) + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination') + end + + it 'should render second page of pipelines' do + visit namespace_project_pipelines_path(project.namespace, project, page: '2') + wait_for_vue_resource + + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + end end describe 'POST /:project/pipelines' do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 1b352be9331..fb19dac1d6a 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do + include WaitForAjax + let(:user) { create(:user) } let(:author) { create(:user) } let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } @@ -34,40 +36,65 @@ describe 'Dashboard Todos', feature: true do end end - describe 'deleting the todo' do + shared_examples 'deleting the todo' do before do - first('.done-todo').click + first('.js-done-todo').click + end + + it 'is marked as done-reversible in the list' do + expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible') end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') + it 'shows Undo button' do + expect(page).to have_selector('.js-undo-todo', visible: true) + expect(page).to have_selector('.js-done-todo', visible: false) end - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) + it 'updates todo count' do + expect(page).to have_content 'To do 0' + expect(page).to have_content 'Done 1' + end + + it 'has not "All done" message' do + expect(page).not_to have_selector('.todos-all-done') end end - context 'todo is stale on the page' do + shared_examples 'deleting and restoring the todo' do before do - todos = TodosFinder.new(user, state: :pending).execute - TodoService.new.mark_todos_as_done(todos, user) + first('.js-done-todo').click + wait_for_ajax + first('.js-undo-todo').click end - describe 'deleting the todo' do - before do - first('.done-todo').click - end + it 'is marked back as pending in the list' do + expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible') + expect(page).to have_selector('.todos-list .todo.todo-pending') + end - it 'is removed from the list' do - expect(page).not_to have_selector('.todos-list .todo') - end + it 'shows Done button' do + expect(page).to have_selector('.js-undo-todo', visible: false) + expect(page).to have_selector('.js-done-todo', visible: true) + end - it 'shows "All done" message' do - expect(page).to have_selector('.todos-all-done', count: 1) - end + it 'updates todo count' do + expect(page).to have_content 'To do 1' + expect(page).to have_content 'Done 0' end end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + + context 'todo is stale on the page' do + before do + todos = TodosFinder.new(user, state: :pending).execute + TodoService.new.mark_todos_as_done(todos, user) + end + + it_behaves_like 'deleting the todo' + it_behaves_like 'deleting and restoring the todo' + end end context 'User has Todos with labels spanning multiple projects' do @@ -113,18 +140,6 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_selector('.gl-pagination .page', count: 2) end - describe 'completing last todo from last page', js: true do - it 'redirects to the previous page' do - visit dashboard_todos_path(page: 2) - expect(page).to have_css("#todo_#{Todo.last.id}") - - click_link('Done') - - expect(current_path).to eq dashboard_todos_path - expect(page).to have_css("#todo_#{Todo.first.id}") - end - end - describe 'mark all as done', js: true do before do visit dashboard_todos_path diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 12ab1d6dde8..2a008427478 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -136,10 +136,10 @@ describe IssuesFinder do end end - context 'filtering by issue iid' do - let(:params) { { search: issue3.to_reference } } + context 'filtering by issues iids' do + let(:params) { { iids: issue3.iid } } - it 'returns issue with iid match' do + it 'returns issues with iids match' do expect(issues).to contain_exactly(issue3) end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 3dcd7781e5b..21ef94ac5d1 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -38,5 +38,13 @@ describe MergeRequestsFinder do merge_requests = MergeRequestsFinder.new(user, params).execute expect(merge_requests.size).to eq(3) end + + it 'filters by iid' do + params = { project_id: project1.id, iids: merge_request1.iid } + + merge_requests = MergeRequestsFinder.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request1) + end end end diff --git a/spec/fixtures/api/schemas/user/login.json b/spec/fixtures/api/schemas/user/login.json index e6c1d9c9d84..6181b3ccc86 100644 --- a/spec/fixtures/api/schemas/user/login.json +++ b/spec/fixtures/api/schemas/user/login.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/fixtures/api/schemas/user/public.json b/spec/fixtures/api/schemas/user/public.json index dbd5d32e89c..5587cfec61a 100644 --- a/spec/fixtures/api/schemas/user/public.json +++ b/spec/fixtures/api/schemas/user/public.json @@ -19,7 +19,6 @@ "organization", "last_sign_in_at", "confirmed_at", - "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", @@ -32,14 +31,14 @@ "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { + "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }, "name": { "type": "string" }, - "state": { + "state": { "type": "string", - "enum": ["active", "blocked"] + "enum": ["active", "blocked"] }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" }, @@ -54,18 +53,17 @@ "organization": { "type": ["string", "null"] }, "last_sign_in_at": { "type": "date" }, "confirmed_at": { "type": ["date", "null"] }, - "theme_id": { "type": "integer" }, "color_scheme_id": { "type": "integer" }, "projects_limit": { "type": "integer" }, "current_sign_in_at": { "type": "date" }, - "identities": { + "identities": { "type": "array", "items": { "type": "object", "properties": { - "provider": { + "provider": { "type": "string", - "enum": ["github", "bitbucket", "google_oauth2"] + "enum": ["github", "bitbucket", "google_oauth2"] }, "extern_uid": { "type": ["number", "string"] } } @@ -74,6 +72,6 @@ "can_create_group": { "type": "boolean" }, "can_create_project": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, - "external": { "type": "boolean" } + "external": { "type": "boolean" } } } diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 1f02e06e312..f3e79cc7290 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -26,32 +26,6 @@ describe PreferencesHelper do end end - describe 'user_application_theme' do - context 'with a user' do - it "returns user's theme's css_class" do - stub_user(theme_id: 3) - - expect(helper.user_application_theme).to eq 'ui_green' - end - - it 'returns the default when id is invalid' do - stub_user(theme_id: Gitlab::Themes.count + 5) - - allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2) - - expect(helper.user_application_theme).to eq 'ui_charcoal' - end - end - - context 'without a user' do - it 'returns the default theme' do - stub_user - - expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class - end - end - end - describe 'user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 001cd8d6325..e5826f9c29f 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -229,4 +229,4 @@ require('./fixtures/emoji_menu'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 4a3da9e318b..3deaf258cae 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -18,4 +18,4 @@ require('~/behaviors/autosize'); return $(document).trigger('load'); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0e4c2c560cc..4820ce41ade 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -103,4 +103,4 @@ require('~/behaviors/quick_submit'); return $.Event('keydown', $.extend({}, defaults, options)); }; }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 631fca06514..3a84013a2ed 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -36,4 +36,4 @@ require('~/behaviors/requires_input'); return expect($('.submit')).not.toBeDisabled(); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 deleted file mode 100644 index c0bdb89ed63..00000000000 --- a/spec/javascripts/dashboard_spec.js.es6 +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-new */ - -require('~/sidebar'); -require('~/lib/utils/text_utility'); - -((global) => { - describe('Dashboard', () => { - const fixtureTemplate = 'static/dashboard.html.raw'; - - function todosCountText() { - return $('.js-todos-count').text(); - } - - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - new global.Sidebar(); - }); - - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); - expect(todosCountText()).toEqual('5'); - }); - - it('should display todos-count with delimiter', () => { - triggerToggle(1000); - expect(todosCountText()).toEqual('1,000'); - - triggerToggle(1000000); - expect(todosCountText()).toEqual('1,000,000'); - }); - }); -})(window.gl); diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index ba5eb81defc..60f6b9b78e3 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -42,4 +42,4 @@ require('~/extensions/array'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index c0bb0419814..096d3272eac 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -39,4 +39,4 @@ require('~/extensions/jquery'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml deleted file mode 100644 index 32446acfd60..00000000000 --- a/spec/javascripts/fixtures/dashboard.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -%ul.nav.nav-sidebar - %li.home.active - %a.dashboard-shortcuts-projects - %span - Projects - %li - %a - %span - Todos - %span.count.js-todos-count - 1 - %li - %a.dashboard-shortcuts-activity - %span - Activity - %li - %a - %span - Groups - %li - %a - %span - Milestones - %li - %a.dashboard-shortcuts-issues - %span - Issues - %span - 1 - %li - %a.dashboard-shortcuts-merge_requests - %span - Merge Requests - %li - %a - %span - Snippets - %li - %a - %span - Help - %li - %a - %span - Profile Settings diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js index 2ef242901e8..a50812d9517 100644 --- a/spec/javascripts/fixtures/emoji_menu.js +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -1,4 +1,4 @@ /* eslint-disable space-before-function-paren */ (function() { window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; -}).call(this); +}).call(window); diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml deleted file mode 100644 index 9d1f7877116..00000000000 --- a/spec/javascripts/fixtures/project_title.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.header-content - %h1.title - %a - GitLab Org - %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} - GitLab Test - %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle{ "data-toggle" => "dropdown", "data-target" => ".header-content", "data-order-by" => "last_activity_at" } - .js-dropdown-menu-projects - .dropdown-menu.dropdown-select.dropdown-menu-projects - .dropdown-title - %span Go to a project - %button.dropdown-title-button.dropdown-menu-close{"aria-label" => "Close", type: "button"} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{id: "", placeholder: "Search your projects", type: "search", value: ""} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times.dropdown-input-clear.js-dropdown-input-clear{role: "button"} - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 index c61c32f8a13..5dfa4008fbd 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js.es6 +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -1,3 +1,5 @@ +/* eslint no-param-reassign: "off" */ + require('~/gfm_auto_complete'); require('vendor/jquery.caret'); require('vendor/jquery.atwho'); @@ -63,6 +65,61 @@ describe('GfmAutoComplete', function () { }); }); + describe('DefaultOptions.matcher', function () { + const defaultMatcher = (context, flag, subtext) => ( + GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) + ); + + const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; + const otherFlags = ['/', ':']; + const flags = flagsUseDefaultMatcher.concat(otherFlags); + + const flagsHash = flags.reduce((hash, el) => { hash[el] = null; return hash; }, {}); + const atwhoInstance = { setting: {}, app: { controllers: flagsHash } }; + + const minLen = 1; + const maxLen = 20; + const argumentSize = [minLen, maxLen / 2, maxLen]; + + const allowedSymbols = ['', 'a', 'n', 'z', 'A', 'Z', 'N', '0', '5', '9', 'А', 'а', 'Я', 'я', '.', '\'', '+', '-', '_']; + const jointAllowedSymbols = allowedSymbols.join(''); + + describe('should match regular symbols', () => { + flagsUseDefaultMatcher.forEach((flag) => { + allowedSymbols.forEach((symbol) => { + argumentSize.forEach((size) => { + const query = new Array(size + 1).join(symbol); + const subtext = flag + query; + + it(`matches argument "${flag}" with query "${subtext}"`, () => { + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(query); + }); + }); + }); + + it(`matches combination of allowed symbols for flag "${flag}"`, () => { + const subtext = flag + jointAllowedSymbols; + + expect(defaultMatcher(atwhoInstance, flag, subtext)).toBe(jointAllowedSymbols); + }); + }); + }); + + describe('should not match special sequences', () => { + const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + + flagsUseDefaultMatcher.forEach((atSign) => { + ShouldNotBeFollowedBy.forEach((followedSymbol) => { + const seq = atSign + followedSymbol; + + it(`should not match "${seq}"`, () => { + expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); + }); + }); + }); + }); + }); + describe('isLoading', function () { it('should be true with loading data object item', function () { expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 2b263b71b7d..576b6cfefdc 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -50,4 +50,4 @@ require('~/lib/utils/text_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index beb544468ef..e7530f61385 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -165,4 +165,4 @@ require('~/issue'); expect($('.issue_counter')).toHaveText(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 8b196f7720f..a0b2ebc221b 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -227,4 +227,4 @@ require('~/line_highlighter'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 25cfa9e9479..fd97dced870 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -6,9 +6,9 @@ require('~/merge_request'); (function() { describe('MergeRequest', function() { return describe('task lists', function() { - preloadFixtures('static/merge_requests_show.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { - loadFixtures('static/merge_requests_show.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); return this.merge = new MergeRequest(); }); it('modifies the Markdown field', function() { @@ -19,11 +19,11 @@ require('~/merge_request'); return it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('/foo'); + expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); return expect(req.data.merge_request.description).not.toBe(null); }); return $('.js-task-list-field').trigger('tasklist:changed'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 5b0c124962c..7506e6ab49e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -209,4 +209,4 @@ require('vendor/jquery.scrollTo'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 8cefdd2409d..d5193b41c33 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -189,4 +189,4 @@ require('~/lib/utils/datetime_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 1d014502c2a..f132537b943 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -166,4 +166,4 @@ require('~/new_branch_form'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index af495787c54..d81a5bbb6a5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -35,15 +35,13 @@ require('~/lib/utils/text_utility'); expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - it('submits the form on tasklist:changed', function() { - var submitted = false; - $('form').on('submit', function(e) { - submitted = true; - e.preventDefault(); + it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1'); + return expect(req.data.note).not.toBe(null); }); - $('.js-task-list-field').trigger('tasklist:changed'); - expect(submitted).toBe(true); }); }); @@ -75,4 +73,4 @@ require('~/lib/utils/text_utility'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6 deleted file mode 100644 index 24833b4eb57..00000000000 --- a/spec/javascripts/project_dashboard_spec.js.es6 +++ /dev/null @@ -1,86 +0,0 @@ -require('~/sidebar'); - -(() => { - describe('Project dashboard page', () => { - let $pageWithSidebar = null; - let $sidebarToggle = null; - let sidebar = null; - const fixtureTemplate = 'projects/dashboard.html.raw'; - - const assertSidebarStateExpanded = (shouldBeExpanded) => { - expect(sidebar.isExpanded).toBe(shouldBeExpanded); - expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded); - }; - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - loadFixtures(fixtureTemplate); - - $pageWithSidebar = $('.page-with-sidebar'); - $sidebarToggle = $('.toggle-nav-collapse'); - - // otherwise instantiating the Sidebar for the second time - // won't do anything, as the Sidebar is a singleton class - gl.Sidebar.singleton = null; - sidebar = new gl.Sidebar(); - }); - - it('can show the sidebar when the toggler is clicked', () => { - assertSidebarStateExpanded(false); - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - }); - - it('should dismiss the sidebar when clone button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const cloneButton = $('.project-clone-holder a.clone-dropdown-btn'); - cloneButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when download button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const downloadButton = $('.project-action-button .btn:has(i.fa-download)'); - downloadButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when add button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const addButton = $('.project-action-button .btn:has(i.fa-plus)'); - addButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when notification button clicked', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - const notifButton = $('.js-notification-toggle-btns .notifications-btn'); - notifButton.click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the body', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('body').click(); - assertSidebarStateExpanded(false); - }); - - it('should dismiss the sidebar when clicking on the project description header', () => { - $sidebarToggle.click(); - assertSidebarStateExpanded(true); - - $('.project-home-panel').click(); - assertSidebarStateExpanded(false); - }); - }); -})(); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index bfe3d2df79d..69d9587771f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -10,11 +10,11 @@ require('~/project'); (function() { describe('Project Title', function() { - preloadFixtures('static/project_title.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); loadJSONFixtures('projects.json'); beforeEach(function() { - loadFixtures('static/project_title.html.raw'); + loadFixtures('issues/open-issue.html.raw'); window.gon = {}; window.gon.api_version = 'v3'; @@ -38,15 +38,12 @@ require('~/project'); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); - it('to show on toggle click', (function(_this) { - return function() { - $('.js-projects-dropdown-toggle').click(); - return expect($('.header-content').hasClass('open')).toBe(true); - }; - })(this)); - return it('hide dropdown', function() { - $(".dropdown-menu-close-icon").click(); - return expect($('.header-content').hasClass('open')).toBe(false); + it('toggles dropdown', function() { + var menu = $('.js-dropdown-menu-projects'); + $('.js-projects-dropdown-toggle').click(); + expect(menu).toHaveClass('open'); + menu.find('.dropdown-menu-close-icon').click(); + expect(menu).not.toHaveClass('open'); }); }); @@ -54,4 +51,4 @@ require('~/project'); window.gon = {}; }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 9284af8a8d9..4ac7e911740 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -79,4 +79,4 @@ require('~/extensions/jquery.js'); expect(todoToggleSpy.calls.count()).toEqual(1); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 9572b52ec1e..cb8754581c0 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -175,4 +175,4 @@ require('vendor/fuzzaldrin-plus'); expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 602ac01aec3..ffff643e371 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -79,4 +79,4 @@ require('~/shortcuts_issuable'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index c0c3837d1f4..cea223bd243 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -41,4 +41,4 @@ require('~/syntax_highlight'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index cba1af4daa4..af2d02b6b29 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -69,4 +69,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 287bfb4138b..6677fe9c1ee 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -30,4 +30,4 @@ return MockU2FDevice; })(); -}).call(this); +}).call(window); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 10578c2c4b5..0f390c8b980 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -74,4 +74,4 @@ require('./mock_u2f_device'); }); }); }); -}).call(this); +}).call(window); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index ce33a6814aa..99515f2e5f2 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -76,4 +76,4 @@ require('~/zen_mode'); keyCode: 27 })); }; -}).call(this); +}).call(window); diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 456dbac0698..11607d4fb26 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -311,7 +311,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end - describe '#issues_per_Project' do + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do doc = Nokogiri::HTML.fragment('') @@ -346,4 +346,26 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end end end + + describe '.references_in' do + let(:merge_request) { create(:merge_request) } + + it 'yields valid references' do + expect do |b| + described_class.references_in(issue.to_reference, &b) + end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData) + end + + it "doesn't yield invalid references" do + expect do |b| + described_class.references_in('#0', &b) + end.not_to yield_control + end + + it "doesn't yield unsupported references" do + expect do |b| + described_class.references_in(merge_request.to_reference, &b) + end.not_to yield_control + end + end end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 6c71e98066b..91c43f2bdc0 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -17,5 +17,31 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + + context 'commit author_url' do + context 'when no commit present' do + let(:build) { create(:ci_build) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit present but has no author' do + let(:build) { create(:ci_build, :with_commit) } + + it 'sets to mailing address of git_author_email' do + expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}") + end + end + + context 'when commit and author are present' do + let(:build) { create(:ci_build, :with_commit_and_author) } + + it 'sets to GitLab user url' do + expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username)) + end + end + end end end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index e6e33d3686a..cc38872e426 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::CommentFormatter, lib: true do + let(:client) { double } let(:project) { create(:empty_project) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } let(:base) do @@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do } end - subject(:comment) { described_class.new(project, raw)} + subject(:comment) { described_class.new(project, raw, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end describe '#attributes' do context 'when do not reference a portion of the diff' do @@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do context 'when author is a GitLab user' do let(:raw) { double(base.merge(user: octocat)) } - it 'returns GitLab user id as author_id' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index afd78abdc9b..33d83d6d2f1 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat) allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) @@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) end - let(:octocat) { double(id: 123456, login: 'octocat') } + + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:label1) do @@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index eec1fabab54..f34d09f2c1d 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::IssueFormatter, lib: true do + let(:client) { double } let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data) } + subject(:issue) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do @@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do expect(issue.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when it has a milestone' do @@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(issue.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 90947ff4707..e46be18aa99 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do + let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } @@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do @@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data) } + subject(:pull_request) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do @@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb new file mode 100644 index 00000000000..db792233657 --- /dev/null +++ b/spec/lib/gitlab/github_import/user_formatter_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::UserFormatter, lib: true do + let(:client) { double } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } + + subject(:user) { described_class.new(client, octocat) } + + before do + allow(client).to receive(:user).and_return(octocat) + end + + describe '#gitlab_id' do + context 'when GitHub user is a GitLab user' do + it 'return GitLab user id when user associated their account with GitHub' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when user primary email matches GitHub email' do + gl_user = create(:user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when any of user linked emails matches GitHub email' do + gl_user = create(:user, email: 'johndoe@example.com') + create(:email, user: gl_user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + end + + it 'returns nil when GitHub user is not a GitLab user' do + expect(user.gitlab_id).to be_nil + end + end +end diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json new file mode 100644 index 00000000000..a78836c3c34 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.light.json @@ -0,0 +1,48 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "labels": [ + { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +}
\ No newline at end of file diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 0af13ba8e47..f4a21c24fa1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -3,24 +3,24 @@ include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do - let(:user) { create(:user) } - let(:namespace) { create(:namespace, owner: user) } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } - let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } - let(:restored_project_json) { project_tree_restorer.restore } + before(:context) do + @user = create(:user) - before do - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + RSpec::Mocks.with_temporary_scope do + @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') + allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + @project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project) + @restored_project_json = project_tree_restorer.restore + end end context 'JSON' do it 'restores models based on JSON' do - expect(restored_project_json).to be true + expect(@restored_project_json).to be true end it 'restore correct project features' do - restored_project_json project = Project.find_by_path('project') expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) @@ -31,62 +31,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end it 'has the same label associated to two issues' do - restored_project_json - expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end it 'has milestones associated to two separate issues' do - restored_project_json - expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) end it 'creates a valid pipeline note' do - restored_project_json - expect(Ci::Pipeline.first.notes).not_to be_empty end it 'restores pipelines with missing ref' do - restored_project_json - expect(Ci::Pipeline.where(ref: nil)).not_to be_empty end it 'restores the correct event with symbolised data' do - restored_project_json - expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty end it 'preserves updated_at on issues' do - restored_project_json - issue = Issue.where(description: 'Aliquam enim illo et possimus.').first expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') end it 'contains the merge access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.merge_access_levels).not_to be_empty end it 'contains the push access levels on a protected branch' do - restored_project_json - expect(ProtectedBranch.first.push_access_levels).not_to be_empty end context 'event at forth level of the tree' do let(:event) { Event.where(title: 'test levels').first } - before do - restored_project_json - end - it 'restores the event' do expect(event).not_to be_nil end @@ -99,77 +79,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do it 'has the correct data for merge request st_diffs' do # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ - expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) + expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9) end it 'has labels associated to label links, associated to issues' do - restored_project_json - expect(Label.first.label_links.first.target).not_to be_nil end it 'has project labels' do - restored_project_json - expect(ProjectLabel.count).to eq(2) end it 'has no group labels' do - restored_project_json - expect(GroupLabel.count).to eq(0) end - context 'with group' do - let!(:project) do - create(:empty_project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: create(:group)) - end - - it 'has group labels' do - restored_project_json - - expect(GroupLabel.count).to eq(1) - end - - it 'has label priorities' do - restored_project_json - - expect(GroupLabel.first.priorities).not_to be_empty - end - end - it 'has a project feature' do - restored_project_json - - expect(project.project_feature).not_to be_nil + expect(@project.project_feature).not_to be_nil end it 'restores the correct service' do - restored_project_json - expect(CustomIssueTrackerService.first).not_to be_nil end context 'Merge requests' do - before do - restored_project_json - end - it 'always has the new project as a target' do - expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project) end it 'has the same source project as originally if source/target are the same' do - expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) + expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project) end it 'has the new project as target if source/target differ' do - expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) + expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project) end it 'has no source if source/target differ' do @@ -177,39 +120,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original - - restored_project_json + context 'tokens are regenerated' do + it 'has a new CI trigger token' do + expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + end - expect(shared.errors.first).not_to include('test') - end + it 'has a new CI build token' do + expect(Ci::Build.where(token: 'abcd')).to be_empty end end + end + end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'Light JSON' do + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } - expect(restored_project_json).to be true - end - end + before do + allow(ImportExport).to receive(:project_filename).and_return('project.light.json') + allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') + end + + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original - context 'tokens are regenerated' do - before do restored_project_json - end - it 'has a new CI trigger token' do - expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + expect(shared.errors.first).not_to include('test') end + end + end - it 'has a new CI build token' do - expect(Ci::Build.where(token: 'abcd')).to be_empty - end + context 'when there is an existing build with build token' do + it 'restores project json correctly' do + create(:ci_build, token: 'abcd') + + expect(restored_project_json).to be true + end + end + + context 'with group' do + let!(:project) do + create(:empty_project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + restored_project_json + end + + it 'has group labels' do + expect(GroupLabel.count).to eq(1) + end + + it 'has label priorities' do + expect(GroupLabel.first.priorities).not_to be_empty end end end diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb index 1e4954c4af8..d7f77486b3e 100644 --- a/spec/lib/gitlab/slash_commands/extractor_spec.rb +++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb @@ -81,6 +81,14 @@ describe Gitlab::SlashCommands::Extractor do let(:original_msg) { "/assign @joe\nworld" } let(:final_msg) { "world" } end + + it 'allows slash in command arguments' do + msg = "/assign @joe / @jane\nworld" + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['assign', '@joe / @jane']] + expect(msg).to eq 'world' + end end context 'in the middle of content' do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb deleted file mode 100644 index 7a140518dd2..00000000000 --- a/spec/lib/gitlab/themes_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Themes, lib: true do - describe '.body_classes' do - it 'returns a space-separated list of class names' do - css = described_class.body_classes - - expect(css).to include('ui_graphite') - expect(css).to include(' ui_charcoal ') - expect(css).to include(' ui_blue') - end - end - - describe '.by_id' do - it 'returns a Theme by its ID' do - expect(described_class.by_id(1).name).to eq 'Graphite' - expect(described_class.by_id(6).name).to eq 'Blue' - end - end - - describe '.default' do - it 'returns the default application theme' do - allow(described_class).to receive(:default_id).and_return(2) - expect(described_class.default.id).to eq 2 - end - - it 'prevents an infinite loop when configuration default is invalid' do - default = described_class::APPLICATION_DEFAULT - themes = described_class::THEMES - - config = double(default_theme: 0).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - - config = double(default_theme: themes.size + 5).as_null_object - allow(Gitlab).to receive(:config).and_return(config) - expect(described_class.default.id).to eq default - end - end - - describe '.each' do - it 'passes the block to the THEMES Array' do - ids = [] - described_class.each { |theme| ids << theme.id } - expect(ids).not_to be_empty - end - end -end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4080092405d..2dfca8bcfce 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Ci::Build, :models do + let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } @@ -207,14 +208,16 @@ describe Ci::Build, :models do end it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + project.add_developer(user) + + retried_rspec = Ci::Build.retry(rspec_test, user) + + expect(staging.depends_on_builds.map(&:id)) + .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) end end describe '#detailed_status' do - let(:user) { create(:user) } - it 'returns a detailed status' do expect(build.detailed_status(user)) .to be_a Gitlab::Ci::Status::Build::Cancelable @@ -813,12 +816,16 @@ describe Ci::Build, :models do subject { build.other_actions } + before do + project.add_developer(user) + end + it 'returns other actions' do is_expected.to contain_exactly(other_build) end context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } + let!(:new_build) { Ci::Build.retry(build, user) } it 'does not return any of them' do is_expected.not_to include(build, new_build) @@ -826,7 +833,7 @@ describe Ci::Build, :models do end context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } + let!(:retried_build) { Ci::Build.retry(other_build, user) } it 'returns a retried build' do is_expected.to contain_exactly(retried_build) @@ -857,21 +864,29 @@ describe Ci::Build, :models do describe '#play' do let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - subject { build.play } + before do + project.add_developer(user) + end + + context 'when build is manual' do + it 'enqueues a build' do + new_build = build.play(user) - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) + expect(new_build).to be_pending + expect(new_build).to eq(build) + end end - context 'for successful build' do + context 'when build is passed' do before do build.update(status: 'success') end it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) + new_build = build.play(user) + + expect(new_build).to be_pending + expect(new_build).not_to eq(build) end end end @@ -1246,12 +1261,9 @@ describe Ci::Build, :models do end context 'when build has user' do - let(:user) { create(:user, username: 'starter') } let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] + [ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } ] end before do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 426be74cd02..10c2bfbb400 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' describe Ci::Pipeline, models: true do include EmailHelpers - let(:project) { FactoryGirl.create :empty_project } - let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project } + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, status: :created, project: project) + end it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } @@ -503,7 +507,9 @@ describe Ci::Pipeline, models: true do end describe '#status' do - let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } + let(:build) do + create(:ci_build, :created, pipeline: pipeline, name: 'test') + end subject { pipeline.reload.status } @@ -545,13 +551,21 @@ describe Ci::Pipeline, models: true do build.cancel end - it { is_expected.to eq('canceled') } + context 'when build is pending' do + let(:build) do + create(:ci_build, :pending, pipeline: pipeline) + end + + it { is_expected.to eq('canceled') } + end end context 'on failure and build retry' do before do build.drop - Ci::Build.retry(build) + project.add_developer(user) + + Ci::Build.retry(build, user) end # We are changing a state: created > failed > running @@ -563,8 +577,6 @@ describe Ci::Pipeline, models: true do end describe '#detailed_status' do - let(:user) { create(:user) } - subject { pipeline.detailed_status(user) } context 'when pipeline is created' do @@ -720,7 +732,7 @@ describe Ci::Pipeline, models: true do describe '#cancel_running' do let(:latest_status) { pipeline.statuses.pluck(:status) } - context 'when there is a running external job and created build' do + context 'when there is a running external job and a regular job' do before do create(:ci_build, :running, pipeline: pipeline) create(:generic_commit_status, :running, pipeline: pipeline) @@ -733,7 +745,7 @@ describe Ci::Pipeline, models: true do end end - context 'when builds are in different stages' do + context 'when jobs are in different stages' do before do create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) @@ -745,17 +757,34 @@ describe Ci::Pipeline, models: true do expect(latest_status).to contain_exactly('canceled', 'canceled') end end + + context 'when there are created builds present in the pipeline' do + before do + create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) + create(:ci_build, :created, stage_idx: 1, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'cancels created builds' do + expect(latest_status).to eq ['canceled', 'canceled'] + end + end end describe '#retry_failed' do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } + before do + project.add_developer(user) + end + context 'when there is a failed build and failed external status' do before do create(:ci_build, :failed, name: 'build', pipeline: pipeline) create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries only build' do @@ -768,11 +797,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end @@ -781,11 +810,11 @@ describe Ci::Pipeline, models: true do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) - pipeline.retry_failed(create(:user)) + pipeline.retry_failed(user) end it 'retries both builds' do - expect(latest_status).to contain_exactly('pending', 'pending') + expect(latest_status).to contain_exactly('pending', 'created') end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3f32248e52b..f8513ac8b1c 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -290,7 +290,7 @@ describe Ci::Runner, models: true do let!(:last_update) { runner.ensure_runner_queue_value } before do - runner.update(description: 'new runner') + Ci::UpdateRunnerService.new(runner).update(description: 'new runner') end it 'sets a new last_update value' do @@ -318,6 +318,25 @@ describe Ci::Runner, models: true do end end + describe '#destroy' do + let(:runner) { create(:ci_runner) } + + context 'when there is a tick in the queue' do + let!(:queue_key) { runner.send(:runner_queue_key) } + + before do + runner.tick_runner_queue + runner.destroy + end + + it 'cleans up the queue' do + Gitlab::Redis.with do |redis| + expect(redis.get(queue_key)).to be_nil + end + end + end + end + describe '.assignable_for' do let(:runner) { create(:ci_runner) } let(:project) { create(:empty_project) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index bf4394f7d5b..36533bdd11e 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CommitStatus, models: true do +describe CommitStatus, :models do let(:project) { create(:project, :repository) } let(:pipeline) do @@ -127,7 +127,7 @@ describe CommitStatus, models: true do end describe '.latest' do - subject { CommitStatus.latest.order(:id) } + subject { described_class.latest.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -143,7 +143,7 @@ describe CommitStatus, models: true do end describe '.running_or_pending' do - subject { CommitStatus.running_or_pending.order(:id) } + subject { described_class.running_or_pending.order(:id) } let(:statuses) do [create_status(name: 'aa', ref: 'bb', status: 'running'), @@ -159,7 +159,21 @@ describe CommitStatus, models: true do end describe '.exclude_ignored' do - subject { CommitStatus.exclude_ignored.order(:id) } + subject { described_class.after_stage(0) } + + let(:statuses) do + [create_status(name: 'aa', stage_idx: 0), + create_status(name: 'cc', stage_idx: 1), + create_status(name: 'aa', stage_idx: 2)] + end + + it 'returns statuses from second and third stage' do + is_expected.to eq(statuses.values_at(1, 2)) + end + end + + describe '.exclude_ignored' do + subject { described_class.exclude_ignored.order(:id) } let(:statuses) do [create_status(when: 'manual', status: 'skipped'), diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index 32935bc0b09..b6e5c95d18a 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -14,8 +14,9 @@ describe Issue, 'Spammable' do end describe 'InstanceMethods' do + let(:issue) { build(:issue, spam: true) } + it 'should be invalid if spam' do - issue = build(:issue, spam: true) expect(issue.valid?).to be_falsey end @@ -29,5 +30,20 @@ describe Issue, 'Spammable' do expect(issue.check_for_spam?).to eq(false) end end + + describe '#submittable_as_spam_by?' do + let(:admin) { build(:admin) } + let(:user) { build(:user) } + + before do + allow(issue).to receive(:submittable_as_spam?).and_return(true) + end + + it 'tests if the user can submit spam' do + expect(issue.submittable_as_spam_by?(admin)).to be(true) + expect(issue.submittable_as_spam_by?(user)).to be(false) + expect(issue.submittable_as_spam_by?(nil)).to be_nil + end + end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 960f29f3805..f0ed0c679d5 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -155,7 +155,7 @@ describe Environment, models: true do end describe '#stop_with_action!' do - let(:user) { create(:user) } + let(:user) { create(:admin) } subject { environment.stop_with_action!(user) } diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 50ad5013df9..3bd7ec18ae0 100644 --- a/spec/models/project_services/chat_message/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -11,21 +11,28 @@ describe ChatMessage::BuildMessage do project_name: 'project_name', project_url: 'http://example.gitlab.com', + build_id: 1, + build_name: build_name, + build_stage: stage, commit: { status: status, author_name: 'hacker', + author_url: 'http://example.gitlab.com/hacker', duration: duration, }, } end let(:message) { build_message } + let(:stage) { 'test' } + let(:status) { 'success' } + let(:build_name) { 'rspec' } + let(:duration) { 10 } context 'build succeeded' do let(:status) { 'success' } let(:color) { 'good' } - let(:duration) { 10 } let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do @@ -38,7 +45,6 @@ describe ChatMessage::BuildMessage do context 'build failed' do let(:status) { 'failed' } let(:color) { 'danger' } - let(:duration) { 10 } it 'returns a message with information about failed build' do expect(subject.pretext).to be_empty @@ -47,11 +53,25 @@ describe ChatMessage::BuildMessage do end end - def build_message(status_text = status) + it 'returns a message with information on build' do + expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>") + end + + it 'returns a message with stage name' do + expect(subject.fallback).to include("of stage #{stage}") + end + + it 'returns a message with link to author' do + expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>") + end + + def build_message(status_text = status, stage_text = stage, build_text = build_name) "<http://example.gitlab.com|project_name>:" \ " Commit <http://example.gitlab.com/commit/" \ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \ " of <http://example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \ + " on build <http://example.gitlab.com/builds/1|#{build_text}>" \ + " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 89cef7ab978..584a4facd94 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -582,18 +582,16 @@ describe User, models: true do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) - expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end describe 'with default overrides' do - let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) } + let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true) } it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey - expect(user.theme_id).to eq(1) end end diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index e487297748b..919c98d6437 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -48,6 +48,7 @@ describe API::AccessRequests, api: true do get api("/#{source_type.pluralize}/#{source.id}/access_requests", master) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index c8e8f31cc1f..6cc1ef315db 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -34,6 +34,7 @@ describe API::AwardEmoji, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(downvote.name) end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index c14c3cb1ce7..71df534ebe1 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -55,6 +55,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(board.id) @@ -72,6 +73,7 @@ describe API::Boards, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['label']['name']).to eq(dev_label.title) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 3e66236f6ae..2e6db0f43c6 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -17,8 +17,10 @@ describe API::Branches, api: true do it "returns an array of project branches" do project.repository.expire_all_method_caches - get api("/projects/#{project.id}/repository/branches", user) + get api("/projects/#{project.id}/repository/branches", user), per_page: 100 + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array branch_names = json_response.map { |x| x['name'] } expect(branch_names).to match_array(project.repository.branch_names) diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 7c9078b2864..921d8714173 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -25,6 +25,7 @@ describe API::BroadcastMessages, api: true do get api('/broadcast_messages', admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_kind_of(Array) expect(json_response.first.keys) .to match_array(%w(id message starts_at ends_at color font active)) diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 834c4e52693..38aef7f2767 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -22,6 +22,7 @@ describe API::Builds, api: true do context 'authorized user' do it 'returns project builds' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array end @@ -97,6 +98,7 @@ describe API::Builds, api: true do it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq 2 end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index eb53fd71872..81a8856b8f1 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -5,12 +5,15 @@ describe API::CommitStatuses, api: true do let!(:project) { create(:project, :repository) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, pipeline: pipeline) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } let(:sha) { commit.id } + let(:commit_status) do + create(:commit_status, status: :pending, pipeline: pipeline) + end + describe "GET /projects/:id/repository/commits/:sha/statuses" do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } @@ -18,10 +21,6 @@ describe API::CommitStatuses, api: true do let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } - it_behaves_like 'a paginated resources' do - let(:request) { get api(get_url, reporter) } - end - context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -42,6 +41,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) json_response.sort_by!{ |status| status['id'] } @@ -54,7 +54,7 @@ describe API::CommitStatuses, api: true do it 'returns all commit statuses' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status1.id, status2.id, status3.id, status4.id, @@ -67,7 +67,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses for specific ref' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status5.id) end @@ -78,7 +78,7 @@ describe API::CommitStatuses, api: true do it 'return latest commit statuses for specific name' do expect(response).to have_http_status(200) - + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status4.id, status5.id) end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 3eef10c0698..ecc6a597869 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,7 +72,7 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits?since=invalid-date", user) expect(response).to have_http_status(400) - expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" + expect(json_response['error']).to eq('since is invalid') end end @@ -456,6 +456,7 @@ describe API::Commits, api: true do it 'returns merge_request comments' do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq('a comment on a commit') diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 766234d7104..7e682e91bd1 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -35,6 +35,7 @@ describe API::DeployKeys, api: true do get api('/deploy_keys', admin) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) end @@ -48,6 +49,7 @@ describe API::DeployKeys, api: true do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end @@ -146,25 +148,4 @@ describe API::DeployKeys, api: true do end end end - - describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) - - expect(response).to have_http_status(404) - end - end - end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 31e3cfa1b2f..e55575ffbda 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -14,14 +14,11 @@ describe API::Deployments, api: true do describe 'GET /projects/:id/deployments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/deployments", user) } - end - it 'returns projects deployments' do get api("/projects/#{project.id}/deployments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['iid']).to eq(deployment.iid) diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 8168b613766..d0958d39d44 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,14 +14,11 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/environments", user) } - end - it 'returns project environments' do get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['name']).to eq(environment.name) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index ccd7898586c..a59112579e5 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -33,6 +33,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response) @@ -43,6 +44,7 @@ describe API::Groups, api: true do get api("/groups", user1), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include 'statistics' end @@ -53,6 +55,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -61,6 +64,7 @@ describe API::Groups, api: true do get api("/groups", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end @@ -78,6 +82,7 @@ describe API::Groups, api: true do get api("/groups", admin), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response) .to satisfy_one { |group| group['statistics'] == attributes } @@ -89,6 +94,7 @@ describe API::Groups, api: true do get api("/groups", admin), skip_groups: [group2.id] expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -103,6 +109,7 @@ describe API::Groups, api: true do get api("/groups", user1), all_available: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to contain_exactly(public_group.name, group1.name) end @@ -120,6 +127,7 @@ describe API::Groups, api: true do get api("/groups", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group3.name, group1.name]) end @@ -128,6 +136,7 @@ describe API::Groups, api: true do get api("/groups", user1), sort: "desc" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end @@ -136,6 +145,7 @@ describe API::Groups, api: true do get api("/groups", user1), order_by: "path" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_groups).to eq([group1.name, group3.name]) end @@ -156,6 +166,7 @@ describe API::Groups, api: true do get api('/groups/owned', user2) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(group2.name) end @@ -290,6 +301,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) @@ -300,6 +312,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1), simple: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(2) project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) @@ -312,6 +325,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user1), visibility: 'public' expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an(Array) expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(public_project.name) @@ -335,6 +349,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.id}/projects", user3) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project3.name) end @@ -365,6 +380,7 @@ describe API::Groups, api: true do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project2.name) end @@ -381,6 +397,7 @@ describe API::Groups, api: true do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers project_names = json_response.map { |proj| proj['name' ] } expect(project_names).to match_array([project1.name, project3.name]) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index cca00df9591..ece1b43567d 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -68,7 +68,9 @@ describe API::Issues, api: true do context "when authenticated" do it "returns an array of issues" do get api("/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(issue.title) expect(json_response.last).to have_key('web_url') @@ -76,7 +78,9 @@ describe API::Issues, api: true do it 'returns an array of closed issues' do get api('/issues?state=closed', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -84,7 +88,9 @@ describe API::Issues, api: true do it 'returns an array of opened issues' do get api('/issues?state=opened', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -92,7 +98,9 @@ describe API::Issues, api: true do it 'returns an array of all issues' do get api('/issues?state=all', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -101,31 +109,44 @@ describe API::Issues, api: true do it 'returns an array of labeled issues' do get api("/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled issues when at least one label matches' do - get api("/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) end it 'returns an empty array if no issue matches labels' do get api('/issues?labels=foo,bar', user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an array of labeled issues matching given state' do get api("/issues?labels=#{label.title}&state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) @@ -134,7 +155,9 @@ describe API::Issues, api: true do it 'returns an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -143,6 +166,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -151,6 +175,7 @@ describe API::Issues, api: true do get api("/issues?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -159,6 +184,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -170,6 +196,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -179,6 +206,7 @@ describe API::Issues, api: true do get api("/issues?milestone=#{no_milestone_title}", author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) @@ -186,36 +214,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api('/issues', user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api('/issues?sort=asc', user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api('/issues?order_by=updated_at', user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api('/issues?order_by=updated_at&sort=asc', user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -269,6 +301,7 @@ describe API::Issues, api: true do get api(base_url, non_member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(group_issue.title) @@ -278,6 +311,7 @@ describe API::Issues, api: true do get api(base_url, author) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -286,6 +320,7 @@ describe API::Issues, api: true do get api(base_url, assignee) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -294,6 +329,7 @@ describe API::Issues, api: true do get api(base_url, user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -302,6 +338,7 @@ describe API::Issues, api: true do get api(base_url, admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -310,6 +347,7 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([group_label.title]) @@ -319,14 +357,31 @@ describe API::Issues, api: true do get api("#{base_url}?labels=#{group_label.title},foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: group_project) + label_c = create(:label, title: 'bar', project: group_project) + + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_c, target: group_issue) + + get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) + end + it 'returns an empty array if no group issue matches labels' do get api("#{base_url}?labels=foo,bar", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -335,6 +390,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -343,6 +399,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=foo", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -351,6 +408,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{group_milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_issue.id) @@ -361,6 +419,7 @@ describe API::Issues, api: true do '&state=closed', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_closed_issue.id) @@ -370,6 +429,7 @@ describe API::Issues, api: true do get api("#{base_url}?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(group_confidential_issue.id) @@ -377,36 +437,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api(base_url, user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api("#{base_url}?sort=asc", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api("#{base_url}?order_by=updated_at", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api("#{base_url}?order_by=updated_at&sort=asc", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end @@ -430,12 +494,17 @@ describe API::Issues, api: true do get api("/projects/#{restricted_project.id}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response).to eq([]) end it 'returns project issues without confidential issues for non project members' do get api("#{base_url}/issues", non_member) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -443,7 +512,9 @@ describe API::Issues, api: true do it 'returns project issues without confidential issues for project members with guest role' do get api("#{base_url}/issues", guest) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq(issue.title) @@ -451,7 +522,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for author' do get api("#{base_url}/issues", author) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -459,7 +532,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -467,7 +542,9 @@ describe API::Issues, api: true do it 'returns project issues with confidential issues for project members' do get api("#{base_url}/issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -475,7 +552,9 @@ describe API::Issues, api: true do it 'returns project confidential issues for admin' do get api("#{base_url}/issues", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) @@ -483,38 +562,61 @@ describe API::Issues, api: true do it 'returns an array of labeled project issues' do get api("#{base_url}/issues?labels=#{label.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues where all labels match' do - get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) + end + + it 'returns an empty array if not all labels matches' do + get api("#{base_url}/issues?labels=#{label.title},foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'returns an empty array if no project issue matches labels' do get api("#{base_url}/issues?labels=foo,bar", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if no issue matches milestone' do get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end it 'returns an empty array if milestone does not exist' do get api("#{base_url}/issues?milestone=foo", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -523,6 +625,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{milestone.title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['id']).to eq(issue.id) @@ -530,9 +633,10 @@ describe API::Issues, api: true do end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}"\ - '&state=closed', user) + get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_issue.id) @@ -542,6 +646,7 @@ describe API::Issues, api: true do get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(confidential_issue.id) @@ -549,36 +654,40 @@ describe API::Issues, api: true do it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts ascending when requested' do get api("#{base_url}/issues?sort=asc", user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end it 'sorts by updated_at descending when requested' do get api("#{base_url}/issues?order_by=updated_at", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort.reverse) end it 'sorts by updated_at ascending when requested' do get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(response_dates).to eq(response_dates.sort) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index a8cd787f398..5d7a76cf3be 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -30,6 +30,7 @@ describe API::Labels, api: true do get api("/projects/#{project.id}/labels", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.first.keys).to match_array expected_keys diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 3e9bcfd1a60..31166b50033 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -34,9 +34,12 @@ describe API::Members, api: true do context "when authenticated as a #{type}" do it 'returns 200' do user = public_send(type) + get api("/#{source_type.pluralize}/#{source.id}/members", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -49,6 +52,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id] end @@ -57,6 +62,8 @@ describe API::Members, api: true do get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.count).to eq(1) expect(json_response.first['username']).to eq(master.username) end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index e1887138aab..1d02e827183 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -19,6 +19,8 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.merge_request_diffs.size) expect(json_response.first['id']).to eq(merge_request_diff.id) expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ff10e79e417..f4dee4a4ca1 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -27,7 +27,9 @@ describe API::MergeRequests, api: true do context "when authenticated" do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -43,7 +45,9 @@ describe API::MergeRequests, api: true do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response.last['title']).to eq(merge_request.title) @@ -51,7 +55,9 @@ describe API::MergeRequests, api: true do it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.last['title']).to eq(merge_request.title) @@ -59,7 +65,9 @@ describe API::MergeRequests, api: true do it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_closed.title) @@ -67,7 +75,9 @@ describe API::MergeRequests, api: true do it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(merge_request_merged.title) @@ -91,7 +101,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -100,7 +112,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -109,7 +123,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } @@ -118,7 +134,9 @@ describe API::MergeRequests, api: true do it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) response_dates = json_response.map{ |merge_request| merge_request['created_at'] } @@ -191,6 +209,8 @@ describe API::MergeRequests, api: true do commit = merge_request.commits.first expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.commits.size) expect(json_response.first['id']).to eq(commit.id) expect(json_response.first['title']).to eq(commit.title) @@ -205,6 +225,7 @@ describe API::MergeRequests, api: true do describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'returns the change information of the merge_request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end @@ -572,7 +593,9 @@ describe API::MergeRequests, api: true do it "returns merge_request comments ordered by created_at" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq("a comment on a MR") @@ -594,7 +617,9 @@ describe API::MergeRequests, api: true do end get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(issue.id) @@ -602,7 +627,9 @@ describe API::MergeRequests, api: true do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -616,6 +643,7 @@ describe API::MergeRequests, api: true do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['title']).to eq(issue.title) diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 8beef821d6c..418bf5a507c 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -14,6 +14,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(milestone.title) end @@ -28,6 +29,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=active", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(milestone.id) @@ -37,25 +39,18 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones?state=closed", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(closed_milestone.id) end - end - - describe 'GET /projects/:id/milestones/:milestone_id' do - it 'returns a project milestone by id' do - get api("/projects/#{project.id}/milestones/#{milestone.id}", user) - - expect(response).to have_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end it 'returns a project milestone by iid' do get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers + expect(json_response.size).to eq(1) expect(json_response.size).to eq(1) expect(json_response.first['title']).to eq closed_milestone.title expect(json_response.first['id']).to eq closed_milestone.id @@ -70,6 +65,26 @@ describe API::Milestones, api: true do expect(json_response.first['id']).to eq milestone.id end + it 'returns a project milestone by iid array' do + get api("/projects/#{project.id}/milestones", user), iid: [milestone.iid, closed_milestone.iid] + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.size).to eq(2) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + end + + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'returns a project milestone by id' do + get api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) + end + it 'returns 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") @@ -177,6 +192,7 @@ describe API::Milestones, api: true do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['milestone']['title']).to eq(milestone.title) end @@ -202,6 +218,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(2) expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) @@ -214,6 +231,7 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) @@ -223,10 +241,47 @@ describe API::Milestones, api: true do get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.map { |issue| issue['id'] }).to include(issue.id) end end end + + describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do + let(:merge_request) { create(:merge_request, source_project: project) } + before do + milestone.merge_requests << merge_request + end + + it 'returns project merge_requests for a particular milestone' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq(merge_request.title) + expect(json_response.first['milestone']['title']).to eq(milestone.title) + end + + it 'returns a 404 error if milestone id not found' do + get api("/projects/#{project.id}/milestones/1234/merge_requests", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 if the user has no access to the milestone' do + new_user = create :user + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user) + + expect(response).to have_http_status(404) + end + + it 'returns a 401 error if user not authenticated' do + get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests") + + expect(response).to have_http_status(401) + end + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index a945d56f529..da8fa06d0af 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -18,17 +18,19 @@ describe API::Namespaces, api: true do context "when authenticated as admin" do it "admin: returns an array of all namespaces" do get api("/namespaces", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(Namespace.count) end it "admin: returns an array of matched namespaces" do get api("/namespaces?search=#{group2.name}", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) expect(json_response.last['path']).to eq(group2.path) expect(json_response.last['full_path']).to eq(group2.full_path) @@ -38,17 +40,19 @@ describe API::Namespaces, api: true do context "when authenticated as a regular user" do it "user: returns an array of namespaces" do get api("/namespaces", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end it "admin: returns an array of matched namespaces" do get api("/namespaces?search=#{user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(1) end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0353ebea9e5..3cca4468be7 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -32,15 +32,12 @@ describe API::Notes, api: true do before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } - end - context "when noteable is an Issue" do it "returns an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) end @@ -56,6 +53,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to be_empty end @@ -75,6 +73,7 @@ describe API::Notes, api: true do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) end @@ -87,6 +86,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) end @@ -109,6 +109,7 @@ describe API::Notes, api: true do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index b7a0b5a9e13..98d004b572e 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -15,15 +15,12 @@ describe API::Pipelines, api: true do before { project.team << [user, :master] } describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/pipelines", user) } - end - context 'authorized user' do it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['id']).to eq pipeline.id diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index f4973d71088..20c76bd2c05 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -25,6 +25,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response.count).to eq(1) expect(json_response.first['url']).to eq("http://example.com") expect(json_response.first['issues_events']).to eq(true) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index eea76c7bb94..f56876bcf54 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -16,9 +16,11 @@ describe API::ProjectSnippets, api: true do internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) - get api("/projects/#{project.id}/snippets/", user) + get api("/projects/#{project.id}/snippets", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.last).to have_key('web_url') @@ -28,7 +30,10 @@ describe API::ProjectSnippets, api: true do create(:project_snippet, :private, project: project) get api("/projects/#{project.id}/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 741815a780e..4e90aae9279 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -76,6 +76,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('tag_list') end @@ -84,6 +85,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include('open_issues_count') end @@ -94,6 +96,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response.status).to eq 200 + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count') end @@ -102,6 +105,7 @@ describe API::Projects, api: true do get api('/projects', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end @@ -110,6 +114,7 @@ describe API::Projects, api: true do get api('/projects', user), statistics: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first).to include 'statistics' end @@ -121,6 +126,7 @@ describe API::Projects, api: true do get api('/projects?simple=true', user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to match_array expected_keys end @@ -131,6 +137,7 @@ describe API::Projects, api: true do get api('/projects', user), { search: project.name } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -141,6 +148,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'private' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(project.id, project2.id, project3.id) end @@ -151,6 +159,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'internal' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(project2.id) end @@ -159,6 +168,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'public' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id) end @@ -169,6 +179,7 @@ describe API::Projects, api: true do get api('/projects', user), { order_by: 'id', sort: 'desc' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['id']).to eq(project3.id) end @@ -179,6 +190,7 @@ describe API::Projects, api: true do get api('/projects', user4), owned: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(project4.name) expect(json_response.first['owner']['username']).to eq(user4.username) @@ -197,6 +209,7 @@ describe API::Projects, api: true do get api('/projects', user3), starred: true expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) end @@ -223,6 +236,7 @@ describe API::Projects, api: true do get api('/projects', user), { visibility: 'public', owned: true, starred: true, search: 'gitlab' } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq(project5.id) @@ -644,9 +658,10 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/events", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_event = json_response.first - expect(first_event['action_name']).to eq('commented on') expect(first_event['note']['body']).to eq('What an awesome day!') @@ -699,11 +714,11 @@ describe API::Projects, api: true do get api("/projects/#{project.id}/users", current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) first_user = json_response.first - expect(first_user['username']).to eq(member.username) expect(first_user['name']).to eq(member.name) expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) @@ -746,7 +761,9 @@ describe API::Projects, api: true do it 'returns an array of project snippets' do get api("/projects/#{project.id}/snippets", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(snippet.title) end @@ -1218,7 +1235,7 @@ describe API::Projects, api: true do end end - describe 'DELETE /projects/:id/star' do + describe 'POST /projects/:id/unstar' do context 'on a starred project' do before do user.toggle_star(project) @@ -1226,16 +1243,16 @@ describe API::Projects, api: true do end it 'unstars the project' do - expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['star_count']).to eq(0) end end context 'on an unstarred project' do it 'does not modify the star count' do - expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count } expect(response).to have_http_status(304) end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index c61208e395c..7652606a491 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -19,10 +19,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array first_commit = json_response.first - - expect(json_response).to be_an Array expect(first_commit['name']).to eq('bar') expect(first_commit['type']).to eq('tree') expect(first_commit['mode']).to eq('040000') @@ -49,6 +49,7 @@ describe API::Repositories, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array + expect(response).to include_pagination_headers expect(json_response[4]['name']).to eq('html') expect(json_response[4]['path']).to eq('files/html') expect(json_response[4]['type']).to eq('tree') @@ -380,10 +381,10 @@ describe API::Repositories, api: true do get api(route, current_user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array first_contributor = json_response.first - expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') expect(first_contributor['name']).to eq('tiagonbotelho') expect(first_contributor['commits']).to eq(1) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index f2d81a28cb8..103d6755888 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -37,18 +37,20 @@ describe API::Runners, api: true do context 'authorized user' do it 'returns user available runners' do get api('/runners', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end it 'filters runners by scope' do get api('/runners?scope=active', user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -73,9 +75,10 @@ describe API::Runners, api: true do context 'with admin privileges' do it 'returns all runners' do get api('/runners/all', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end @@ -91,9 +94,10 @@ describe API::Runners, api: true do it 'filters runners by scope' do get api('/runners/all?scope=specific', admin) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_falsey end @@ -183,6 +187,7 @@ describe API::Runners, api: true do it 'updates runner' do description = shared_runner.description active = shared_runner.active + runner_queue_value = shared_runner.ensure_runner_queue_value update_runner(shared_runner.id, admin, description: "#{description}_updated", active: !active, @@ -197,18 +202,24 @@ describe API::Runners, api: true do expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.run_untagged?).to be(false) expect(shared_runner.locked?).to be(true) + expect(shared_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end context 'when runner is not shared' do it 'updates runner' do description = specific_runner.description + runner_queue_value = specific_runner.ensure_runner_queue_value + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response).to have_http_status(200) expect(specific_runner.description).to eq('test') expect(specific_runner.description).not_to eq(description) + expect(specific_runner.ensure_runner_queue_value) + .not_to eq(runner_queue_value) end end @@ -335,9 +346,10 @@ describe API::Runners, api: true do context 'authorized user with master privileges' do it "returns project's runners" do get api("/projects/#{project.id}/runners", user) - shared = json_response.any?{ |r| r['is_shared'] } + shared = json_response.any?{ |r| r['is_shared'] } expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(shared).to be_truthy end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 6b9a739b439..1ef92930b3c 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -13,6 +13,8 @@ describe API::Snippets, api: true do get api("/snippets/", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, internal_snippet.id, @@ -25,7 +27,10 @@ describe API::Snippets, api: true do create(:personal_snippet, :private) get api("/snippets/", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.size).to eq(0) end end @@ -43,6 +48,8 @@ describe API::Snippets, api: true do get api("/snippets/public", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( public_snippet.id, public_snippet_other.id) diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index b3e5afdadb1..b59da632c00 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -31,6 +31,7 @@ describe API::SystemHooks, api: true do get api("/hooks", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be true diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 898d2b27e5c..8a4f078182f 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -20,10 +20,9 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", current_user) expect(response).to have_http_status(200) - - first_tag = json_response.first - - expect(first_tag['name']).to eq(tag_name) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) end end @@ -43,7 +42,9 @@ describe API::Tags, api: true do context 'without releases' do it "returns an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) end @@ -59,6 +60,7 @@ describe API::Tags, api: true do get api("/projects/#{project.id}/repository/tags", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) expect(json_response.first['message']).to eq('Version 1.1.0') diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index c0a8c0832bb..8506e8fccde 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -22,6 +22,7 @@ describe API::Templates, api: true do get api('/templates/gitignores') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to be > 15 end @@ -32,6 +33,7 @@ describe API::Templates, api: true do get api('/templates/gitlab_ci_ymls') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['name']).not_to be_nil end @@ -69,6 +71,7 @@ describe API::Templates, api: true do get api('/templates/licenses') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(15) expect(json_response.map { |l| l['key'] }).to include('agpl-3.0') @@ -80,6 +83,7 @@ describe API::Templates, api: true do get api('/templates/licenses?popular=1') expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.map { |l| l['key'] }).to include('apache-2.0') diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 56dc017ce54..2069d2a7c75 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -33,6 +33,7 @@ describe API::Todos, api: true do get api('/todos', john_doe) expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) expect(json_response[0]['id']).to eq(pending_3.id) @@ -52,6 +53,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { author_id: author_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) end @@ -64,6 +66,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { type: 'MergeRequest' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -74,6 +77,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { state: 'done' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -84,6 +88,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { project_id: project_2.id } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -94,6 +99,7 @@ describe API::Todos, api: true do get api('/todos', john_doe), { action: 'mentioned' } expect(response.status).to eq(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 84104aa66ee..92dfc2aa277 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -100,6 +100,7 @@ describe API::Triggers do get api("/projects/#{project.id}/triggers", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_a(Array) expect(json_response[0]).to have_key('token') end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5958012672e..603da9f49fc 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -40,7 +40,9 @@ describe API::Users, api: true do it "returns an array of users" do get api("/users", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array username = user.username expect(json_response.detect do |user| @@ -55,13 +57,16 @@ describe API::Users, api: true do get api("/users?blocked=true", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/)) end it "returns one user" do get api("/users?username=#{omniauth_user.username}", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['username']).to eq(omniauth_user.username) end @@ -70,7 +75,9 @@ describe API::Users, api: true do context "when admin" do it "returns an array of users" do get api("/users", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first.keys).to include 'email' expect(json_response.first.keys).to include 'organization' @@ -87,6 +94,7 @@ describe API::Users, api: true do get api("/users?external=true", admin) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response).to all(include('external' => true)) end @@ -507,8 +515,11 @@ describe API::Users, api: true do it 'returns array of ssh keys' do user.keys << key user.save + get api("/users/#{user.id}/keys", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(key.title) end @@ -595,8 +606,11 @@ describe API::Users, api: true do it 'returns array of emails' do user.emails << email user.save + get api("/users/#{user.id}/emails", admin) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['email']).to eq(email.email) end @@ -774,8 +788,11 @@ describe API::Users, api: true do it "returns array of ssh keys" do user.keys << key user.save + get api("/user/keys", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["title"]).to eq(key.title) end @@ -891,8 +908,11 @@ describe API::Users, api: true do it "returns array of emails" do user.emails << email user.save + get api("/user/emails", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first["email"]).to eq(email.email) end @@ -983,69 +1003,69 @@ describe API::Users, api: true do end end - describe 'PUT /users/:id/block' do + describe 'POST /users/:id/block' do before { admin } it 'blocks existing user' do - put api("/users/#{user.id}/block", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('blocked') end it 'does not re-block ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/block", admin) + post api("/users/#{ldap_blocked_user.id}/block", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/block", user) + post api("/users/#{user.id}/block", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end end - describe 'PUT /users/:id/unblock' do + describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } before { admin } it 'unblocks existing user' do - put api("/users/#{user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('active') end it 'unblocks a blocked user' do - put api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(blocked_user.reload.state).to eq('active') end it 'does not unblock ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/unblock", admin) + post api("/users/#{ldap_blocked_user.id}/unblock", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/unblock", user) + post api("/users/#{user.id}/unblock", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - put api("/users/ASDF/block", admin) + post api("/users/ASDF/block", admin) expect(response).to have_http_status(404) end @@ -1073,14 +1093,14 @@ describe API::Users, api: true do end context "as a user than can see the event's project" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/users/#{user.id}/events", user) } - end - context 'joined event' do it 'returns the "joined" event' do get api("/users/#{user.id}/events", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } expect(comment_event['project_id'].to_i).to eq(project.id) diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb new file mode 100644 index 00000000000..8aaf3be4f87 --- /dev/null +++ b/spec/requests/api/v3/boards_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe API::V3::Boards, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user) } + let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } + + let!(:dev_label) do + create(:label, title: 'Development', color: '#FFAABB', project: project) + end + + let!(:test_label) do + create(:label, title: 'Testing', color: '#FFAACC', project: project) + end + + let!(:dev_list) do + create(:list, label: dev_label, position: 1) + end + + let!(:test_list) do + create(:list, label: test_label, position: 2) + end + + let!(:board) do + create(:board, project: project, lists: [dev_list, test_list]) + end + + before do + project.team << [user, :reporter] + project.team << [guest, :guest] + end + + describe "GET /projects/:id/boards" do + let(:base_url) { "/projects/#{project.id}/boards" } + + context "when unauthenticated" do + it "returns authentication error" do + get v3_api(base_url) + + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns the project issue board" do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(board.id) + expect(json_response.first['lists']).to be_an Array + expect(json_response.first['lists'].length).to eq(2) + expect(json_response.first['lists'].last).to have_key('position') + end + end + end + + describe "GET /projects/:id/boards/:board_id/lists" do + let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + + it 'returns issue board lists' do + get v3_api(base_url, user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get v3_api("/projects/#{project.id}/boards/22343/lists", user) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb new file mode 100644 index 00000000000..0e4c6bc3bc6 --- /dev/null +++ b/spec/requests/api/v3/branches_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Branches, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/branches" do + it "returns an array of project branches" do + project.repository.expire_all_method_caches + + get v3_api("/projects/#{project.id}/repository/branches", user), per_page: 100 + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + end +end diff --git a/spec/requests/api/v3/labels_spec.rb b/spec/requests/api/v3/labels_spec.rb new file mode 100644 index 00000000000..18e2c0d40c8 --- /dev/null +++ b/spec/requests/api/v3/labels_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe API::V3::Labels, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let!(:label1) { create(:label, title: 'label1', project: project) } + let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/labels' do + it 'returns all available labels to the project' do + group = create(:group) + group_label = create(:group_label, title: 'feature', group: group) + project.update(group: group) + create(:labeled_issue, project: project, labels: [group_label], author: user) + create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed) + create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) + + expected_keys = [ + 'id', 'name', 'color', 'description', + 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count', + 'subscribed', 'priority' + ] + + get v3_api("/projects/#{project.id}/labels", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.first.keys).to match_array expected_keys + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) + + label1_response = json_response.find { |l| l['name'] == label1.title } + group_label_response = json_response.find { |l| l['name'] == group_label.title } + priority_label_response = json_response.find { |l| l['name'] == priority_label.title } + + expect(label1_response['open_issues_count']).to eq(0) + expect(label1_response['closed_issues_count']).to eq(1) + expect(label1_response['open_merge_requests_count']).to eq(0) + expect(label1_response['name']).to eq(label1.name) + expect(label1_response['color']).to be_present + expect(label1_response['description']).to be_nil + expect(label1_response['priority']).to be_nil + expect(label1_response['subscribed']).to be_falsey + + expect(group_label_response['open_issues_count']).to eq(1) + expect(group_label_response['closed_issues_count']).to eq(0) + expect(group_label_response['open_merge_requests_count']).to eq(0) + expect(group_label_response['name']).to eq(group_label.name) + expect(group_label_response['color']).to be_present + expect(group_label_response['description']).to be_nil + expect(group_label_response['priority']).to be_nil + expect(group_label_response['subscribed']).to be_falsey + + expect(priority_label_response['open_issues_count']).to eq(0) + expect(priority_label_response['closed_issues_count']).to eq(0) + expect(priority_label_response['open_merge_requests_count']).to eq(1) + expect(priority_label_response['name']).to eq(priority_label.name) + expect(priority_label_response['color']).to be_present + expect(priority_label_response['description']).to be_nil + expect(priority_label_response['priority']).to eq(3) + expect(priority_label_response['subscribed']).to be_falsey + end + end +end diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb new file mode 100644 index 00000000000..c696721c1c9 --- /dev/null +++ b/spec/requests/api/v3/repositories_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Repositories, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tree" do + let(:route) { "/projects/#{project.id}/repository/tree" } + + shared_examples_for 'repository tree' do + it 'returns the repository tree' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_commit = json_response.first + expect(first_commit['name']).to eq('bar') + expect(first_commit['type']).to eq('tree') + expect(first_commit['mode']).to eq('040000') + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'with recursive=1' do + it 'returns recursive project paths tree' do + get v3_api("#{route}?recursive=1", current_user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response[4]['name']).to eq('html') + expect(json_response[4]['path']).to eq('files/html') + expect(json_response[4]['type']).to eq('tree') + expect(json_response[4]['mode']).to eq('040000') + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + + context 'when ref does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:message) { '404 Tree Not Found' } + end + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository tree' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository tree' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + + describe 'GET /projects/:id/repository/contributors' do + let(:route) { "/projects/#{project.id}/repository/contributors" } + + shared_examples_for 'repository contributors' do + it 'returns valid data' do + get v3_api(route, current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + first_contributor = json_response.first + expect(first_contributor['email']).to eq('tiagonbotelho@hotmail.com') + expect(first_contributor['name']).to eq('tiagonbotelho') + expect(first_contributor['commits']).to eq(1) + expect(first_contributor['additions']).to eq(0) + expect(first_contributor['deletions']).to eq(0) + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository contributors' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository contributors' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end +end diff --git a/spec/requests/api/v3/system_hooks_spec.rb b/spec/requests/api/v3/system_hooks_spec.rb new file mode 100644 index 00000000000..da58efb6ebf --- /dev/null +++ b/spec/requests/api/v3/system_hooks_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe API::V3::SystemHooks, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let!(:hook) { create(:system_hook, url: "http://example.com") } + + before { stub_request(:post, hook.url) } + + describe "GET /hooks" do + context "when no user" do + it "returns authentication error" do + get v3_api("/hooks") + + expect(response).to have_http_status(401) + end + end + + context "when not an admin" do + it "returns forbidden error" do + get v3_api("/hooks", user) + + expect(response).to have_http_status(403) + end + end + + context "when authenticated as admin" do + it "returns an array of hooks" do + get v3_api("/hooks", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['url']).to eq(hook.url) + expect(json_response.first['push_events']).to be true + expect(json_response.first['tag_push_events']).to be false + end + end + end +end diff --git a/spec/requests/api/v3/tags_spec.rb b/spec/requests/api/v3/tags_spec.rb new file mode 100644 index 00000000000..6722789d928 --- /dev/null +++ b/spec/requests/api/v3/tags_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require 'mime/types' + +describe API::V3::Tags, api: true do + include ApiHelpers + include RepoHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, :repository, creator: user) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + + describe "GET /projects/:id/repository/tags" do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + let(:description) { 'Awesome release!' } + + shared_examples_for 'repository tags' do + it 'returns the repository tags' do + get v3_api("/projects/#{project.id}/repository/tags", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'repository tags' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'repository tags' do + let(:current_user) { user } + end + end + + context 'without releases' do + it "returns an array of project tags" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + end + end + + context 'with releases' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + end + + it "returns an array of project tags with release info" do + get v3_api("/projects/#{project.id}/repository/tags", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(tag_name) + expect(json_response.first['message']).to eq('Version 1.1.0') + expect(json_response.first['release']['description']).to eq(description) + end + end + end +end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb new file mode 100644 index 00000000000..5020ef18a3a --- /dev/null +++ b/spec/requests/api/v3/users_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe API::V3::Users, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + let(:email) { create(:email, user: user) } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + + describe 'GET /user/:id/keys' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/keys") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/keys', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of ssh keys' do + user.keys << key + user.save + + get v3_api("/users/#{user.id}/keys", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) + end + end + end + + describe 'GET /user/:id/emails' do + before { admin } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api("/users/#{user.id}/emails") + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get v3_api('/users/999999/emails', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns array of emails' do + user.emails << email + user.save + + get v3_api("/users/#{user.id}/emails", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['email']).to eq(email.email) + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/emails", admin) + + expect(response).to have_http_status(404) + end + end + end + + describe "GET /user/keys" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/keys") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of ssh keys" do + user.keys << key + user.save + + get v3_api("/user/keys", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["title"]).to eq(key.title) + end + end + end + + describe "GET /user/emails" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/user/emails") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns array of emails" do + user.emails << email + user.save + + get v3_api("/user/emails", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first["email"]).to eq(email.email) + end + end + end + + describe 'PUT /users/:id/block' do + before { admin } + it 'blocks existing user' do + put v3_api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('blocked') + end + + it 'does not re-block ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/block", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end + + describe 'PUT /users/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } + before { admin } + + it 'unblocks existing user' do + put v3_api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('active') + end + + it 'unblocks a blocked user' do + put v3_api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(blocked_user.reload.state).to eq('active') + end + + it 'does not unblock ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/unblock", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/block", admin) + + expect(response).to have_http_status(404) + end + end +end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index ebb11166964..ef2ddc4b1d7 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,8 +1,16 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, services: true do - let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') } +describe Ci::ProcessPipelineService, :services do let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'master', project: project) + end + + before do + project.add_developer(user) + end describe '#execute' do context 'start queuing next builds' do @@ -285,7 +293,7 @@ describe Ci::ProcessPipelineService, services: true do expect(builds.pluck(:name)) .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') - Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success expect(builds.pluck(:name)).to contain_exactly( 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb new file mode 100644 index 00000000000..93147870afe --- /dev/null +++ b/spec/services/ci/retry_build_service_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Ci::RetryBuildService, :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + let(:service) do + described_class.new(project, user) + end + + shared_examples 'build duplication' do + let(:build) do + create(:ci_build, :failed, :artifacts, :erased, :trace, + :queued, :coverage, pipeline: pipeline) + end + + describe 'clone attributes' do + described_class::CLONE_ATTRIBUTES.each do |attribute| + it "clones #{attribute} build attribute" do + expect(new_build.send(attribute)).to eq build.send(attribute) + end + end + end + + describe 'reject attributes' do + described_class::REJECT_ATTRIBUTES.each do |attribute| + it "does not clone #{attribute} build attribute" do + expect(new_build.send(attribute)).not_to eq build.send(attribute) + end + end + end + + it 'has correct number of known attributes' do + attributes = + described_class::CLONE_ATTRIBUTES + + described_class::IGNORE_ATTRIBUTES + + described_class::REJECT_ATTRIBUTES + + expect(attributes.size).to eq build.attributes.size + end + end + + describe '#execute' do + let(:new_build) { service.execute(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'enqueues the new build' do + expect(new_build).to be_pending + end + + it 'resolves todos for old build that failed' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close) + + service.execute(build) + end + + context 'when there are subsequent builds that are skipped' do + let!(:subsequent_build) do + create(:ci_build, :skipped, stage_idx: 1, pipeline: pipeline) + end + + it 'resumes pipeline processing in subsequent stages' do + service.execute(build) + + expect(subsequent_build.reload).to be_created + end + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + + describe '#reprocess' do + let(:new_build) { service.reprocess(build) } + + context 'when user has ability to execute build' do + before do + project.add_developer(user) + end + + it_behaves_like 'build duplication' + + it 'creates a new build that represents the old one' do + expect(new_build.name).to eq build.name + end + + it 'does not enqueue the new build' do + expect(new_build).to be_created + end + end + + context 'when user does not have ability to execute build' do + it 'raises an error' do + expect { service.reprocess(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end +end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb new file mode 100644 index 00000000000..c0af8b8450a --- /dev/null +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -0,0 +1,175 @@ +require 'spec_helper' + +describe Ci::RetryPipelineService, '#execute', :services do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:service) { described_class.new(project, user) } + + context 'when user has ability to modify pipeline' do + let(:user) { create(:admin) } + + context 'when there are failed builds in the last stage' do + before do + create_build('rspec 1', :success, 0) + create_build('rspec 2', :failed, 1) + create_build('rspec 3', :canceled, 1) + end + + it 'enqueues all builds in the last stage' do + service.execute(pipeline) + + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_pending + expect(pipeline.reload).to be_running + end + end + + context 'when there are failed or canceled builds in the first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('spinach 1', :canceled, 2) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('spinach 1')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is failed build present which was run on failure' do + before do + create_build('rspec 1', :failed, 0) + create_build('rspec 2', :canceled, 0) + create_build('rspec 3', :canceled, 1) + create_build('report 1', :failed, 2) + end + + it 'retries builds only in the first stage' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('rspec 2')).to be_pending + expect(build('rspec 3')).to be_created + expect(build('report 1')).to be_created + expect(pipeline.reload).to be_running + end + + it 'creates a new job for report job in this case' do + service.execute(pipeline) + + expect(statuses.where(name: 'report 1').first).to be_retried + end + end + + context 'when pipeline contains manual actions' do + context 'when there is a canceled manual action in first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('staging', :canceled, 0, :manual) + create_build('rspec 2', :canceled, 1) + end + + it 'retries builds failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(build('rspec 2')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a skipped manual action in last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :skipped, 1, :manual) + end + + it 'retries canceled job and skips manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the last stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 1, :manual) + end + + it 'retries canceled job and does not update the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_created + expect(pipeline.reload).to be_running + end + end + + context 'when there is a created manual action in the first stage' do + before do + create_build('rspec 1', :canceled, 0) + create_build('staging', :created, 0, :manual) + end + + it 'retries canceled job and skipps the manual action' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_skipped + expect(pipeline.reload).to be_running + end + end + end + + it 'closes all todos about failed jobs for pipeline' do + expect(MergeRequests::AddTodoWhenBuildFailsService) + .to receive_message_chain(:new, :close_all) + + service.execute(pipeline) + end + + it 'reprocesses the pipeline' do + expect(pipeline).to receive(:process!) + + service.execute(pipeline) + end + end + + context 'when user is not allowed to retry pipeline' do + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + def statuses + pipeline.reload.statuses + end + + def build(name) + statuses.latest.find_by(name: name) + end + + def create_build(name, status, stage_num, on = 'on_success') + create(:ci_build, name: name, + status: status, + stage: "stage_#{stage_num}", + stage_idx: stage_num, + when: on, + pipeline: pipeline) do |build| + pipeline.update_status + end + end +end diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb new file mode 100644 index 00000000000..e429fcfc72f --- /dev/null +++ b/spec/services/ci/update_runner_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Ci::UpdateRunnerService, :services do + let(:runner) { create(:ci_runner) } + + describe '#update' do + before do + allow(runner).to receive(:tick_runner_queue) + end + + context 'with description params' do + let(:params) { { description: 'new runner' } } + + it 'updates the runner and ticking the queue' do + expect(update).to be_truthy + + runner.reload + + expect(runner).to have_received(:tick_runner_queue) + expect(runner.description).to eq('new runner') + end + end + + context 'when params are not valid' do + let(:params) { { run_untagged: false } } + + it 'does not update and give false because it is not valid' do + expect(update).to be_falsey + + runner.reload + + expect(runner).not_to have_received(:tick_runner_queue) + expect(runner.run_untagged).to be_truthy + end + end + + def update + described_class.new(runner).update(params) + end + end +end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index cf0a18aacec..6fb4d517115 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -234,7 +234,11 @@ describe CreateDeploymentService, services: true do context 'when build is retried' do it_behaves_like 'does create environment and deployment' do - let(:deployable) { Ci::Build.retry(build) } + before do + project.add_developer(user) + end + + let(:deployable) { Ci::Build.retry(build, user) } subject { deployable.success } end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index bb7830c7eea..d80fb8a1af1 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -17,7 +17,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do described_class.new(project, user, commit_message: 'Awesome message') end - let(:todo_service) { TodoService.new } + let(:todo_service) { spy('todo service') } let(:merge_request) do create(:merge_request, merge_user: user, @@ -107,4 +107,27 @@ describe MergeRequests::AddTodoWhenBuildFailsService do end end end + + describe '#close_all' do + context 'when using pipeline that belongs to merge request' do + it 'resolves todos about failed builds for pipeline' do + service.close_all(pipeline) + + expect(todo_service) + .to have_received(:merge_request_build_retried) + .with(merge_request) + end + end + + context 'when pipeline is not related to merge request' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'does not resolve any todos about failed builds' do + service.close_all(pipeline) + + expect(todo_service) + .not_to have_received(:merge_request_build_retried) + end + end + end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 3faa88c00a1..74bfba44dfd 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -50,6 +50,25 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'when flushing caches fail' do + before do + new_user = create(:user) + project.team.add_user(new_user, Gitlab::Access::DEVELOPER) + allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError) + end + + it 'keeps project team intact upon an error' do + Sidekiq::Testing.inline! do + begin + destroy_project(project, user, {}) + rescue Redis::CannotConnectError + end + end + + expect(project.team.members.count).to eq 1 + end + end + context 'with async_execute' do let(:async) { true } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 4320365ab57..9f24cc0f3f2 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -287,39 +287,51 @@ describe TodoService, services: true do end end - shared_examples 'marking todos as done' do |meth| - let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } - let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + shared_examples 'updating todos state' do |meth, state, new_state| + let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } - it 'marks related todos for the user as done' do + it 'updates related todos for the user with the new_state' do service.send(meth, collection, john_doe) - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done + expect(first_todo.reload.state?(new_state)).to be true + expect(second_todo.reload.state?(new_state)).to be true end describe 'cached counts' do it 'updates when todos change' do - expect(john_doe.todos_done_count).to eq(0) - expect(john_doe.todos_pending_count).to eq(2) + expect(john_doe.todos.where(state: new_state).count).to eq(0) + expect(john_doe.todos.where(state: state).count).to eq(2) expect(john_doe).to receive(:update_todos_count_cache).and_call_original service.send(meth, collection, john_doe) - expect(john_doe.todos_done_count).to eq(2) - expect(john_doe.todos_pending_count).to eq(0) + expect(john_doe.todos.where(state: new_state).count).to eq(2) + expect(john_doe.todos.where(state: state).count).to eq(0) end end end describe '#mark_todos_as_done' do - it_behaves_like 'marking todos as done', :mark_todos_as_done do + it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do let(:collection) { [first_todo, second_todo] } end end describe '#mark_todos_as_done_by_ids' do - it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do + it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do + let(:collection) { [first_todo, second_todo].map(&:id) } + end + end + + describe '#mark_todos_as_pending' do + it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do + let(:collection) { [first_todo, second_todo] } + end + end + + describe '#mark_todos_as_pending_by_ids' do + it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do let(:collection) { [first_todo, second_todo].map(&:id) } end end diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb deleted file mode 100644 index 352a6eeec79..00000000000 --- a/spec/support/api/pagination_shared_examples.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Specs for paginated resources. -# -# Requires an API request: -# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } -shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end -end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 247f0954221..6f31828b825 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -3,6 +3,10 @@ RSpec.configure do |config| DatabaseCleaner.clean_with(:truncation) end + config.append_after(:context) do + DatabaseCleaner.clean_with(:truncation) + end + config.before(:each) do DatabaseCleaner.strategy = :transaction end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae75..cd55d63125e 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "theme_id":2,"color_scheme_id":2, + "color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file +} diff --git a/spec/support/matchers/pagination_matcher.rb b/spec/support/matchers/pagination_matcher.rb new file mode 100644 index 00000000000..60f5e8239a7 --- /dev/null +++ b/spec/support/matchers/pagination_matcher.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :include_pagination_headers do |expected| + match do |actual| + expect(actual.headers).to include('X-Total', 'X-Total-Pages', 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page', 'Link') + end +end diff --git a/yarn.lock b/yarn.lock index 99db6f61bcd..ad4b5223d60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -852,7 +852,7 @@ boom@2.x.x: dependencies: hoek "2.x.x" -bootstrap-sass@3.3.6: +bootstrap-sass@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1" @@ -1265,7 +1265,7 @@ custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" -d3@3.5.11: +d3@^3.5.11: version "3.5.11" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c" @@ -1404,7 +1404,7 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -dropzone@4.2.0: +dropzone@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3" @@ -2273,13 +2273,6 @@ ignore@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410" -imports-loader@^0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.6.5.tgz#ae74653031d59e37b3c2fb2544ac61aeae3530a6" - dependencies: - loader-utils "0.2.x" - source-map "0.1.x" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2624,17 +2617,17 @@ jodid25519@^1.0.0: dependencies: jsbn "~0.1.0" -"jquery-ui@github:jquery/jquery-ui#1.11.4": +"jquery-ui@git+https://github.com/jquery/jquery-ui#1.11.4": version "1.11.4" - resolved "https://codeload.github.com/jquery/jquery-ui/tar.gz/d6713024e16de90ea71dc0544ba34e1df01b4d8a" + resolved "git+https://github.com/jquery/jquery-ui#d6713024e16de90ea71dc0544ba34e1df01b4d8a" -jquery-ujs@1.2.1: +jquery-ujs@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92" dependencies: jquery ">=1.8.0" -jquery@2.2.1, jquery@>=1.8.0: +jquery@^2.2.1, jquery@>=1.8.0: version "2.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f" @@ -3031,7 +3024,7 @@ moment@2.x: version "2.17.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" -mousetrap@1.4.6: +mousetrap@^1.4.6: version "1.4.6" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a" @@ -4296,7 +4289,7 @@ unc-path-regex@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" -underscore@1.8.3: +underscore@^1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" @@ -4387,11 +4380,11 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vue-resource@0.9.3: +vue-resource@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d" -vue@2.0.3: +vue@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde" |